TS스터디 이펙티브 item14~16

온호성·2023년 4월 6일
1

😏item 14 타입 연산과 제너릭 사용으로 반복 줄이기

같은 코드를 반복하지 말라는 DRY(don’t repeat yourself)원칙이 있다. 이 아이템은 타입 반복을 줄여본다는 내용이 대부분이다.

타입 중복 또한 코드 중복만큼 피해야 한다. 타입이 에서 공유된 패턴을 제거하는 일은 js에서 중복된 코드를 제거하는 것보다 생소하게 느껴질 수 있기 때문에 타입 간에 매핑하는 방법을 익힌다면 ts에서도 DRY 원칙을 지킬 수 있다.

타입에 이름 붙이기

function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
  return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));

  //위 코드에서 타입에 이름을 붙여 수정하면
  
interface Point2D{
  x: number;
  y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }

매핑된 타입 사용하기

interface Person {
  
  name: string;
  age: number;
  weight: number;
}

type TopPerson = {
  userId: Person['name'];
  age: Person['age'];
  weight: Person['weight'];
}

type TopPerson = {
  [k in 'name' | 'age' | 'weight']: Person[k] // 매핑된 타입
}

type TopPerson = Pick<Person, 'name' | 'age' | 'weight'>; // 제너릭 타입

[k in T]은 매핑된 타입은 배열의 필드를 순회하는 것 과 같은 방식이다.

태그된 유니온의 태그 타입 꺼내기

아래 코드처럼 태그된 유니온에서 type 속성의 타입을 꺼내고 싶은 경우에 반복이 발생할 수 있다

interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction; // 태그된 유니온
type ActionType = 'save' | 'load'; // 타입의 반복 !

이 경우 Action 유니온을 인덱싱 하여 타입 반복 없이 ActionType 을 정의할 수 있다.

type ActionType = Action['type']; // 타입은  'save' | 'load'

type ActionType = Pick<Action, 'type'>; // 제네릭 타입

매핑된 타입과 keyof

아래 코드는 인스턴스가 생성되고 난 다음 프로퍼티가 업데이트 되는 클래스를 정의하는 경우이다.
이 때 업데이트시 대부분의 타입들이 선택적 필드가 된다.

interface Options {
  width: number;
  height: number;
  color: string;
}
interface OptionsUpdate { // 기존의 Options타입과 동일하면서 대부분이 선택적 필드이다.
  width?: number;
  height?: number;
  color?: string;
}
class UIWidget {
  constructor (init: Options) { /* */ }
  update(options: OptionsUpdate)  { /* */ }
}

매핑된 타입과 keyof 를 사용하면 Options 로부터 OptionsUpdate 를 만들 수 있다.

type OptionsUpdate = { [k in keyof Options]?: Options[k] };

keyof 는 타입을 받아서 속성 타입의 유니온 을 반환한다.

type OptionsKeys = keyof Options; // 'width' | 'height' || 'color'

값의 형태에 해당하는 타입을 정의하고 싶을 때

const INIT_OPTIONS = {
  width: 500,
  height: 550,
  color: '#00FF00',
  label: 'VGA',
};
type Options = typeof INIT_OPTIONS;
/**
 * 다음과 동일
 * interface Options{
 *  width: number;
 *  height: number;
 *  color: string;
 * };
 * /

여기서 사용된 typeof 는 런타임 연산자가 아니라 타입스크립트 단계에서 연산되어 강력한 타입 표현이 가능하다.
그러나 값으로부터 타입을 만들어 낼 때는 선언 순서에 주의해야한다. 타입 정의를 먼저 하고 값이 그 타입에 할당 가능하다고 선언하는 것이 좋다. 그렇게 해야 타입이 더 명확해지고 예상하기 어려운 타입 변동을 방지할 수 있다.

제네릭 표준 라이브러리

함수나 메서드의 반환 값에 명명된 타입을 만들고 싶은 경우 ReturnType 제네릭을 사용하면 된다. 표준 라이브러리에 이런 일반적 패턴의 제네릭 타입이 정의되어 있다.

function getUserInfo(userId: string) {
  // ...
  return {
    userId,
    name,
    age,
    height,
    weight,
  }
}
type UserInfo = ReturnType<typeof getUserInfo>; // getUserInfo 의 타입이 적용됨

매핑된 타입을 생성해주는 Pick 제네릭

간단하게 표현하면 다음과 같다.

type Pick<T, K> = { [k in K]: T[k]};

정확히 표현하면 extends 를 사용해 제네릭 매개변수의 타입을 제한해주어야 한다.

type Pick<T, K extends keyof T> = { [k in K]: T[k] };
type TopState = Pick<State, 'name' | 'age' | 'weight'>;

-요약-

  • DRY를 타입에도 적용
  • 타입에 이름을 붙여 반복을 피하기
  • extends를 붙여 인터페이스의 반복을 피하기
  • mapped type, keyof, typeof, indexing 사용
  • 제네릭 타입을 이용하여 타입들 간 매핑. extends 사용하여 제너릭 제한
  • Pick, Partial, ReturnType등의 표준 제너릭에 익숙해지기

😭item 15 동적 데이터에 인덱스 시그니처 사용하기

인덱스 시그니처는 객체의 키, 밸류를 다음과 같이 표현한 것이다.

type Something = {[property: string]: string};

인덱스 시그니처는 유연하게 타입 매핑을 표현할 수 있다.

type Person = { [property: string]: string };//--> 인덱스 시그니처
const hee: Person = {
  name: 'hee',
  job: 'developer',
  hobby: 'drawing'
}

인덱스 시그니처는 세 가지 의미를 가지고 있다.

  • 키의 이름: 키의 위치만 표시하는 용도로 타입 체커에서는 사용되지 않아 무시할 수 있는 참고 정보라 할 수 있다.
  • 키의 타입: string이나 number 또는 symbol이여야 한다. 보통 string을 사용한다.
  • 값의 타입: 어떤 것이든 될 수 있다.

그러나 이렇게 타입체크를 하게 되면 단점이 있는데

  • 잘못된 키를 포함해서 모든 키를 허용하게 된다. name 대신 Name을 쓰더라도 허용된다.
  • 키는 무엇이든 가능하기에 자동 완성 기능이 동작하지 않는다.
  • 특정 키가 필요하지 않으므로 {}타입도 허용하게 된다.
  • 키마다 다른 타입을 가질 수 없다.
interface Person {
  [key: string]: string;
  name: string;
  age: number;
}
// 'age' 형식의 'number' 속성을 'string' 인덱스 유형 'string'에 할당할 수 없습니다.
  • 언어 서비스를 제공받을 수 없다.

    위와 같은 인덱스 시그니처는 런타임 때까지 객체의 속성을 알 수 없을 경우에만 사용한다. 또 안전한 접근을 위해 undefined 추가를 고려할 수 있지만 이 부분을 체크하는 코드가 필요하다.

대안으로
Record 제네릭 타입 : 키 타입에 유연성을 제공하는 제네릭 타입이다.
Record<’x’ | ‘y’ | ‘z’, number>
→ key 타입에 유연성 제공(값은 number)

매핑된 타입: 키마다 별도의 타입 을 사용할 수 있다.
{ [k in ‘x’ | ‘y’ | ‘z’]: number } or { [k in ‘a’ | ‘b’ | ‘c’]: k extends ‘b’ ? string: number; }

위와 같은 인덱스 시그니처보다 정확한 타입을 사용하는 것이 좋다.

-요약-

  • csv나 json같은 동적 데이터에 활용 (런타임 때까지 객체의 속성을 알 수 없을 경우)
  • 안전한 접근을 위해 인덱스 시그니처의 값 타입에 undefined 추가 고려 (Optional)
  • 가능하다면 인터페이스, Record,매핑된 타입 같은 인덱스 시그니처보다 정확한 타입 사용

😐item 16 number인덱스 시그니처보다는 Array, 튜플, ArrayLike를 사용하기

자바스크립트에서 객체란 키/값 쌍의(string, symbol) 모음이고, 키는 보통 문자열이며 그 값은 어떤 것이든 될 수 있다.

숫자나 다른 타입의 값을 키로 사용하고자 하면 자바스크립트 런타임시 문자열로 변환된다.
이는 배열도 마찬가지로 인덱스로 접근시 x[1] 인덱스가 문자열로 변환되어 x['1'] 사용된다.

타입스크립트는 이런 혼란을 잡기위해 숫자 키를 허용하고, 문자열과 다른 것으로 인식한다.
따라서 타입 체크 시점에서 배열의 인덱스에 문자열을 사용하는 오류를 잡을 수 있다.

const arr = [1, 2, 3];
const arr0 = arr[0]; // ok
const arr1 = arr['1'];  // 오류

자바스크립트에서는 배열도 객체이므로 인덱스로 접근시 숫자가 문자열로 변환된다. x[1] -> x['1'] 타입스크립트는 이와 같은 혼란을 피하기 위해 숫자 키를 허용하고, 문자열 키와 다른 것으로 인식하는데 Object.keys같은 구문은 여전히 문자열로 반환됨.

const xs = [1, 2, 3];

const keys = Object.keys(xs); // keys: string[]

for(const key in xs) {
  key; // key: string 
  const x = xs[key]; // key가 문자열임에도 인덱스로 접근시 타입스크립트에서 오류를 발생하지 않음
}
{ 0: 1, 1: 2, 2: 3 }; // number 인덱스 시그니처 -> 숫자 인덱스를 작성하더라도 어차피 자바스크립트에서는 인덱스로 접근시 문자열로 타입을 변경하여 접근한다. 그리고 타입스크립트는 숫자 인덱스와 문자열 인덱스를 서로 다른것으로 인식하기 때문에 문제가 발생할 수 있다.

const arr = [1, 2, 3]; // 배열 -> 숫자로 인덱스할 항목을 지정하는 경우 위처럼 객체를 사용하지 않고 배열을 사용하는것이 바람직하다.

const arrLike = { // 유사 배열 객체 -> map, forEach 같은 배열 고차함수를 사용하고 싶지 않을때 유사배열 객체를 사용해라. 하지만 키는 여전히 문자열이다.
 '0': 1,
 '1': 2,
 '2': 3,
 length: 3,
}; 

-요약-

  • 배열은 객체이므로 키는 숫자가 아니라 문자열이다.
  • 인덱스 시그니처로 사용된 number타입은 버그를 잡기 위한 순수 타입스크립트 코드이다.
  • 인덱스 시그니처에 number를 사용하기보다 Array, 튜플, ArrayLike 타입을 사용하는 것이 좋다.

0개의 댓글

관련 채용 정보