[이펙티브 타입스크립트] 36~40

공효은·2023년 11월 15일
0

typescript

목록 보기
8/8
post-thumbnail

아이템 36 해당 분야의 용어로 타입 이름 짓기

엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드의 타입과 추상화 수준을 높여준다.
잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어준다.

동물들의 데이터베이스를 구축한다고 가정해보자. 인터페이스는 다음과 같다.

interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

const leopard: Animal = {
  name: 'Snow Leopard',
  endangered: false,
  habitat: 'tundra',
}

문제점

  • name은 매우 일반적인 용어이다. 동물의 학명인지 일반적인 명칭인지 알 수 없다.
  • endangered 속성이 멸종 위기를 표현하기 위해 boolean 타입을 사용한것이 이상하다. 이미 멸종된 동물을 true로 해야하는지 판단할 수 없다.
  • 서식지를 나타내는 habitat 속성은 너무 범위가 넓은 string 타입일 뿐만아니라 서식지 뜻 자체도 불분명하기 때문에 모호하다.
  • 객체의 변수명이 leopard 이지만, name 속성의 값은 'Snow Leopard'이다. 객체의 이름과 속성의 name이 다른 의도로 사용된 것인지 불분명하다.

=> 의도가 뭔지 모르겠다.... 타입스크립트는 의도를 명시하기에 아주 좋은 친구인데 말이지... 잘써먹지 못하고 있다.

이 문제들을 해결하려면 해당 속성을 작성한 사람을 찾아서 물어봐야한다. 하지만 그사람은 이미 없...다..

개선

interface Animal {
  commonName: string;
  genus: string;
  species: string;
  status: ConservationSstatus;
  climates: KoppenClimate[];
}
type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC';
type KoppenClimate = 'Af' | 'Am' | 'As' | 'Aw' | 'BSh' | 'BSk' | 'BWh' | 'BWk' | 
'Cfa' | 'Cfb' | 'Cfc' |'Csa' | 'Csb' | 'Csc' | 'Cwa' | 'Cwb' | 'Cwc' | 'Dsa' | 'Dsb' | 'Dsc' | 'Dwa' | 'Dwb' | 'Dwc' | 'Dwd' | 'EF' | 'ET';
const snowLeopard: Animal = {
  commonName: 'Snow Leopard',
  genus: 'Panthera',
  species: 'Uncia',
  status: 'VU',                  //취약종(vulnerable)
  climates: ['ET', 'EF', 'Dfd'], //고산대(alpine) 또는 아고산대(subalpine) 
}

이 코드는 다음 세 가지를 개선했다.

  • name은 commonName, genus(속), species(종) 등 더 구체적인 용어로 대체했다.
  • endangered는 동물 보호 등급에 대한 IUCN의 표준 분류 체계인 ConservationStatus 타입의 status로 변경되었다.
  • habitat은 기후를 뜻하는 climates로 변경되었으며, 쾨펜 기후 분류를 사용한다.

데이터를 명확하게 표현하고 있고, 정보를 찾기위해 사람에 의존할 필요없다. 온라인에 무수히 많은 정보가 있다.

코드로 표현하고자 하는 모든 분야에는 주제를 설명하기 위한 전문 용어들이 있따. 자체적으로 용어를 만들어 내려고 하지 말고, 해당 분야에 이미 존재하는 용어를 사용해야한다.

타입, 속성, 변수에 이름을 붙일 때 명심해야 할 세 가지 규칙

  • 동일한 의미를 나타낼 때는 같은 용어를 사용해야 한다.
  • data, info, thing, item, object, entity 같은 모호하고 의미 없는 이름은 피하자.
  • 이름을 지을 때는 포함된 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지 고려해야한다. INodeList 보다는 Directory가 더 의미 있다.

아이템 37 공식 명칭에는 상표를 붙이기

구조적 타이핑 특성 때문에 가끔 코드가 이상한 결과를 낼 수 있다.

interface Vector2D {
  x: number;
  y: number;
}

function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y  * p.y);
}

calculateNorm({x: 3, y: 4}); // 정상, 결과는 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D); //정상! 결과는 동일하게 5
  • calculateNorm 함수가 3차원 벡터를 허용하지 않게 하려면 공식 명칭(nomi-nal typing)을 사용하면 된다.
  • 공식 명칭을 사용하는 것은, 타입이 아니라 값의 관점에서 Vector2D라고 말하는 것이다.
  • 공식 명칭 개념을 타입스크립트에서 흉내 내려면 '상표(brand)'를 붙이면 된다.
interface Vector2D {
  _brand: '2d';
  x: number;
  y: number;
}
function vec2D(x: number, y: number): Vector2D {
  return {x, y, _brand: '2d'}
}
function calculateNorm(p: Vector2D){
  return Math.sqrt(p.x * p.x + p.y  * p.y); //기존과 동일하다.
}

calculateNorm(vec2D(3,4)); // 정상, 결과는 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);
             // '_brand' 속성 ... 형식에 없습니다.
  • 상표(_brand)를 사용해서 calculateNorm 함수가 Vector2D 타입만 받는 것을 보장한다.
  • vec3D 값에 _brand: '2d' 라고 추가하는 것 같은 악의적인 사용은 막을 수 없다. 단순한 실수를 방지하기에 충분하다.
  • 상표 기법은 타입시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.

추가 속성을 붙일 수 없는 string 이나 number 같은 내장 타입도 상표화 할 수 있다.

예시) 절대 경로를 사용해 파일 시스템에 접근하는 함수를 가정해 보자.
런타임에는 절대 경로('/')로 시작하는지 체크하기 쉽지만, 타입시스템에서는 절대 경로를 판단하기 어렵기 때문에 상표 기법을 사용한다.

type AbsolutePath = string & {_brand: 'abs'};
function listAbsolutePath(path: AbsolutePath) {
  // ...
}
function isAbsolutePath(path: string): path is AbsolutePath {
  return path.startsWith('/');
}

string 타입이면서 _brand 속성을 가지는 객체를 만들 수는 없다. AbsolutePath는 온전히 타입 시스템의 영역이다.

만약 path 값이 절대 경로와 상대 경로 둘 다 될 수 있다면, 타입을 정제해 주는 타입 가드를 사용해서 오류를 방지할 수 있다.

function f(path: string) {
  if (isAbsolutePath(path)) {
    listAbsolutePath(path);
  }
  listAbsolutePath(path);
 // 'string' 형식의 인수는 'AbsolutePath' 형식에 매개 변수에 할당될 수 없다.
}

로직을 분기하는 대신에 오류가 발생한 곳에 path as AbsolutePath를 사용해서 오류를 제거할 수도 있지만 단언문은 지양해야한다.
단언문을 쓰지 않고 AbsolutePath 타입을 얻기 위한 유일한 방법은 AbsolutePath 타입을 매개변수로 받거나 타입이 맞는지 체크하는것 뿐이다.

5장 any 다루기

  • 전통적으로 프로그래밍 언어들의 타입 시스템은 완전히 정적이거나 완전히 동적으로 확실히 구분되어 있었다. 그러나 타입스크립트의 타입 시스템은 선택적이고 점진적이기 때문에 정적이면서도 동적이다. 따라서 타입스크립트는 프로그램의 일부분에만 타입시스템을 적용할 수 있다.
  • 프로그램의 일부분에만 타입 시스템을 적용할 수 있다는 특성 덕분에 점진적인 마이그레이션(js -> ts) 가능하다.
  • any는 매우 강력하므로 남용하게 될 소지가 높다. 현명한 사용법을 익히자.

아이템 38 any 타입은 가능한 한 좁은 범위에서만 사용하기

function processBar(b:Bar) { /* ... */ }
function f() {
  const x = expressionReturningFoo();
  processBar(x);
  // ~'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다. 
}

문맥 상으로 x라는 변수가 동시에 Foo 타입과 Bar 타입에 할당 가능하다면, 오류를 제거하는 방법은 두가지다.

function f1() {
  const x: any = expressionReturningFoo(); //❌
  processBar(x)
}
function f2() {
  const x = expressionReturningFoo(); 
  processBar(x as any); //차라리 이게 낫다.
}

f1 보다 f2가 나은 이유는 any 타입이 processBar 함수의 매개변수에서만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않기 때문이다.
f1에서는 함수의 마지막까지 x의 타입이 any인 반면, f2에서는 processBar 호출 이후에 x가 그대로 Foo 타입이다.

만일! f1 함수가 x를 반환한다면 문제가 커진다..

function f1() {
  const x: any = expressionReturningFoo();
  processBar(x)
  return x;
}

function g() {
  const foo = f1() //타입이 any
  foo.fooMethod(); // 이 함수 호출은 체크 되지 않는다.
}

g 함수 내에서 f1이 사용되므로 f1의 반환 타입인 any 타입이 foo의 타입에 영향을 미친다. 이렇게 함수에서 any를 반환하면 그 영향력은 프로젝트 전반에 퍼진다..

반면 f2를 사용하면 any 타입이 함수 바깥으로 영향을 미치지 않는다.

⭐️ 타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다. 함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지 할 수 있다.

객체도 살펴보자

const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
    // 'foo' 속성이 'Foo' 타입에 필요하지만 'Bar' 타입에는 없다.
  }
}

얘보단

const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
  }
} as any; //이렇게 하지말자

얘가 낫다.

const config: Config = {
  a: 1,
  b: 2, //이 속성은 여전히 체크된다.
  c: {
    key: value as any;
  }
} 

아이템 39 any를 구체적으로 변형해서 사용하기

any는 자바스크립트에서 표현할 수 있는 모든 값을 아우르는 매우 큰 범위의 타입이다.
any 타입에는 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘리먼트는 물론 null과 undefined 까지 포함된다.
any보다 구체적인 타입을 찾아 타입 안전성을 높여야한다.

function getLengthBad(array: any) { // 이렇게 하지 말자.
  return array.length;
}

function getLength(array: any[]){
  return array.length;
}

any를 사용하는 getLengthBad 보다는 any[]를 사용하는 getLength가 더 좋은 함수이다.

  • 함수 내의 array.length 타입이 체크된다.
  • 함수의 반환 타입이 any 대신 number로 추론된다.
  • 함수 호출될 때 매개변수가 배열인지 체크된다.

배열이 아닌 값을 넣어서 실행해 보면, getLength는 제대로 오류를 표시하지만 getLengthBad는 오류를 잡아내지 못한다.

함수의 매개변수가 객체이긴 하지만 값을 알 수 없다면 {[key: string]: any} 처럼 선언하면 된다.

function hasTwelveLetterkey(o: {[key: string]: any}){
  for(const key in o){
    if(key.length === 12){
      return true;
    }
  }
  return false
}

함수의 매개변수가 객체지만 값을 알수 없다면 {[key: string]: any} 대신 모든 비기본형(non-primitive) 타입을 포함하는 object 타입을 사용할 수도 있다. object 타입은 객체의 키를 열거할 수는 있지만 속성에 접근할 수 없다는 점에서 {[key: string]: any} 와 약간다르다.

function hasTwelveLetterkey(o: object){
  for(const key in o){
    if(key.length === 12){
	  console.log(key, o[key]);
      // '{}' 형식에 인덱스 시그니처가 없으므로 요소에 암시적으로 'any' 형식이 있습니다.
      return true;
    }
  }
  return false
}

함수의 타입에도 단순히 any를 사용해서는 안된다. 최소한 구체화할 수 있는 방법이 세가지 있다.

type Fn0 = () => any; //매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; //매개변수 1개
type FnN = (...args: any[]) => any; //모든 개수의 매개변수
//"Function" 타입과 동일하다.

모두 any보다는 구체적이다. 마지막 줄을 잘 살펴보면 ...args 의 타입을 any[]로 선언했다. any로 선언해도 동작하지만 any[]f로 선언했다. any로 선언해도 동작하지만 any[]로 선언하면 배열 형태라는 것을 알 수 있어 더 구체적이다.

const numArgsBad = (...args: any) => args.length; //any를 반환한다.
const numArgsGood = (...args: any[]) => args.length //number를 반환한다.

아이템 40 함수 안으로 타입 단언문 감추기

함수를 작성하다 보면, 외부로 드러난 타입 정의는 간단하지만 내부 로직이 복잡해서 안전한 타입으로 구현하기 어려운 경우가 만다.

함수의 모든 부분을 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.

함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는 게 낫다.

제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 더 좋은 설계이다.

예) 어떤 함수가 자신의 마지막 호출을 캐시하도록 만든다고 가정해보자.
어떤 함수든 캐싱할 수 있도록 래퍼 함수 cacheLast를 만들어보자.

declare function cacheLast<T extends Function>(fn: T): T;

declare function shaalowEqual(a: any, b:any): boolean;
function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[]|null = null;
  let lastResult: any;
  
  return function(...args: any[]){
    //'(...args: any[]) => any' 형식은 'T' 형식에 할당할 수 없다.
    if(!lastArgs || !shallowEqual(lastArgs, args)){
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  }
}

타입스크립트는 반환문에 있는 함수와 원본 함수 T타입이 어떤 관련이 있는지 알지 못하기 때문에 오류가 발생했다. 그러나 결과적으로 원본 함수 T타입과 동일한 매개변수로 호출되고 반환값 역시 예상한 결과가 되기 떄문에 타입 선언문을 추가해서 오류를 제거하는 것이 문제가 되지 않는다.

declare function cacheLast<T extends Function>(fn: T): T;

declare function shaalowEqual(a: any, b:any): boolean;
function cacheLast<T extends Function>(fn: T): T {
  let lastArgs: any[]|null = null;
  let lastResult: any;
  
  return function(...args: any[]){
    //'(...args: any[]) => any' 형식은 'T' 형식에 할당할 수 없다.
    if(!lastArgs || !shallowEqual(lastArgs, args)){
      lastResult = fn(...args);
      lastArgs = args;
    }
    return lastResult;
  } as unknown as T;
}

함수 내부에는 any가 꽤 많이 보이지만 타입 정의에는 any가 없기 때문에, cacheLast를 호출하는 쪽에서는 any가 사용됐는지 모름

profile
잼나게 코딩하면서 살고 싶어요 ^O^/

0개의 댓글