728x90

조건부 타입 활용하기

타입스크립트의 조건부 타입은 삼항 연산자와 동일하게 Condition ? A: B 형태를 가지는데 A는 Condition 이 true 일 때, 도출되고 B는 false일 때 도출되는 타입이다.

 

조건부 타입을 활용하면 중복되는 타입 코드를 제거하고 상황에 따른 타입을 얻을 수 있다.

 

1. extends와 제네릭을 활용한 조건부 타입

interface Bank {
  financialCode: string;
  companyName: string;
  name: string;
  fullName: string;
}

interface Card {
  financialCode: string;
  companyName: string;
  name: string;
  appCardType?: string;
}

type PayMethod<T> = T extends 'card' ? Card : Bank;
type CardPayMethodType = PayMethod<'card'>;
type BankMethodType = PayMethod<'bank'>;

 

이렇게 사용할경우 인자에 따라 반환되는 타입을 다르게 설정할 수 있다. (유니온으로 설정할 경우 타입스크립트는 인자에 따라 다른 타입을 정확히 추론할 수 없다.)

extends활용 예시는 아래와 같다.

  1. 제네릭과 extends를 함께 사용해 제네릭으로 받는 타입을 제한 => 따라서 개발자는 잘못된 값을 넘길 수 없어서 휴먼에러가 방지됨

  2. extends를 활용해 조건부 타입 설정함.
    조건부 타입을 사용해서 반환 값을 사용자가 원하는 값으로 구체화 할 수 있음 
    => 불필요한 타입 가드, 타입 단언등을 방지할 수 있음.

2. infer를 활용해서 타입 추론하기

infer는 타입을 추론하는 역할을 하고, 삼항 연산자를 사용한 조건문 형태를 가진다.

type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;

const promises = [Promise.resolve('Mark'), Promise.resolve(38)];
type Expected = UnpackPromise<typeof promises>; //type Expected = string | number

 

템플릿 리터럴 타입 활용하기

전장에서 언급된 템플릿 리터럴 타입은 언제 쓸지가 모호했는데, 배민외식업광장에 적용한 예시를 통해 알아보자.

type Direction =
  | 'top'
  | 'topLeft'
  | 'topRight'
  | 'bottom'
  | 'bottomLeft'
  | 'bottomRight';

type Vertical = 'top' | 'bottom';
type Horizon = 'left' | 'right';

type Direction2 = Vertical | `${Vertical}${Capitalize<Horizon>}`;

 

일일이  입력하는 것보다 가독성도 좋고 재사용 수정도 용이하다. 또한 변수에 할당되는 타입을 특정 문자열로 정확히 검사하여 휴먼 에러를 방지하고, 자동 완성 기능을 통해 생산성도 높인다.

 

다만 유니온 타입의 경우 조합이 너무 복잡할 경우에는 타입스크립트 컴파일러가 추론하지 않고 에러를 뱉을 수 있어서, 적절히 나누는게 좋다..

커스텀 유틸리티 타입

Pick과 Omit 같은 유틸리티 타입을 활용하면, 중복된 코드를 작성하지 않아도 되고 유지보수도 편리해진다.

이런 유틸리티 타입처럼 커스텀한 유틸리티 타입을 만들 수 있다.

1. PickOne 유틸리티 함수

type CustomCard = {
  type: 'card';
  card: string;
};

type CustomAccount = {
  type: 'account';
  account: string;
};

function withdraw(type: CustomCard | CustomAccount) {
  //...
}

withdraw({ type: 'card', card: 'hyundai' });

앞선 장에서 유니온 타입으로 할 경우, 둘 중 하나의 타입만 존재해야 하는 경우를 고를 수 없어서 에러가 뜰 수 있고, 이를

해결하기 위한 방법으로 식별할 수 있는 유니온을 사용한다 했다.

하지만 이미 작성된 코드에 위와 같은 방식을 적용하려하면, 많은 부분을 수정해야 한다.

이러한 경우를 위해 PickOne이라는 커스텀 유틸리티 함수를 만들려고 한다.

 

목표 => account : string 혹은 card:string 중 하나만 가진 객체만 허용해야 함.

account 일 떄는 card를 받지 못하고, card 일 떄는 account를 받지 못하게, 다른 타입을 옵셔널한 undefined값으로 지정하는 방식을 사용할 것이다.

type CustomEx1 =
  | { account: string; card?: undefined }
  | { card: string; account?: undefined };

이를 아래와 같은 커스텀 유틸리티 타입을 통해 만들 수 있다.

 

type PickOne<T> = {
  [P in keyof T]: Record<P, T[P]> &
    Partial<Record<Exclude<keyof T, P>, undefined>>;
}[keyof T];

 

위 커스텀 유틸리티 타입은 두가지로 나누어 생각할 수 있다.

#1) One 타입

type exCard = { card: string };

type One<T> = { [P in keyof T]: Record<P, T[P]> }[keyof T];

const one: One<exCard> = { card: 'hyunadi' }; //const one: Record<"card", string>

 

1)  [P in keyof T] : P는 T객체의 키값

2)  Record<P, T[P]>  : P타입을 키로 가지고, value는 P를  키로둔 T객체의 값의 레코드 타입

3)  { [P in keyof T]: Record<P, T[P]> } : 키는 T객체의 키 모음, values는 해당 키의 원본 객체 T

4) 3번타입에서 다시 [keyof T] 키 값으로 접근

 

#2) ExclueOne<T> 타입

type ExcludeOne<T> = {
  [P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>>;
}[keyof T];

 

1)  [P in keyof T] : P는 T객체의 키값

2) Exclude<keyof T, P :  T객체가 가진 키 값에서 P 타입과 일치하는 키 값을 제외 (이를 A라고 하자)

3) Record<Exclude<keyof T, P>, undefined> : 키로 A 타입, 값으로는 undefined를 갖는 레코드 타입(B라고 하자)

4) Partial<Record<Exclude<keyof T, P>, undefined>> : B 타입을 옵셔널로 만듬

5) [P in key of T] 로 매핑된 타입에서 동일한 객체의 키값인 [keyof T] 로 접근하기에 4번 타입이 반환된다.

 

즉 두 함수를 합치면 PickOne이 된다.

type PickOne<T> = One<T> & ExcludeOne<T>;

 

type PickOneCard = {
  card: string;
  test: string;
};

type PickOneAccount = {
  account: string;
};

type CardOrAccount = PickOne<PickOneCard & PickOneAccount>;

function withdrawPick(type: CardOrAccount) {
  //. .
}

withdrawPick({ card: 'hyundaii' });
// withdrawPick({ card: 'hyundaii', account: 'test' });// error

즉 하나의 키값만 뽑아 쓸 수 있다.

 

2. NonNullable 타입 검사 함수를 사용하여 간편하게 타입가드 하기 

type NonNullable<T> = T extends null | undefined ? never : T;

타입 스크립트에서 제공하는 유틸리티 타입으로 제네릭으로 받는 T가 null 또는 undefined 일 떄 never 또는 T를 반환하는 타입이다

이를 활용하여서 타입가드를 효율적으로 할 수 잇는 함수를 만들 수 있다.

function NonNullable<T>(value: T): value is NonNullable<T> {
  return value !== null && value !== undefined;
}

 

불변 객체 타입으로 활용하기

프로젝트에서 상숫값을 관리할 때 객체를 쓴다  ex) theme , 상숫값등등

이때 as const 와 keyof로 객체 타입을 구체적으로 설정하면 타입에 맞지 않는 값을 전달할 경우 타입 에러가 반환되기 떄문에 컴파일 단계에서 발생하는 실수를 방지할 수 있다. 또한 자동 완성 기능을 통해 객체에 어떤 값이 있는지 쉽게 파악할 수 있게 된다.

 

interface ColorType {
  red: string;
  green: string;
  blue: string;
}

type ColorKeyType = keyof ColorType; // "red"|"?green"|"blue"

const colors = {
  red: '#F45452',
  green: '#0C952A',
  blue: '#1A7CFF',
};

type ColorsType = typeof colors; //{    red: string;    green: string;    blue: string;}

 

interface ColorType {
  red: string;
  green: string;
  blue: string;
}

type ColorKeyType = keyof ColorType; // "red"|"?green"|"blue"

const colors = {
  black: '#000000',
  white: '#FFFFFF',
  red: '#F45452',
  green: '#0C952A',
  blue: '#1A7CFF',
};

type ColorsType = typeof colors; //{black: string; white: string; red: string; green: string;blue: string;}

const theme = {
  colors: {
    default: colors.black,
    ...colors,
  },
  backgroundColor: {
    default: colors.white,
    red: colors.red,
    green: colors.green,
    black: colors.black,
  },
};

type ColorType2 = keyof typeof theme.colors; //"black" | "white" | "red" | "green" | "blue" | "default"

 

 

 

Record 원시 타입 키 개선하기

Record를 쓸 떄 string이나 number같은 타입을 명시하는데, 이럴 경우 키가 유효하지 않더라도 오류를 표시하지 않는다.

 

type FoodCategory = string;
interface Food {
  name: string;
}

const foodByCategory: Record<FoodCategory, Food[]> = {
  한식: [{ name: '제육' }, { name: '뚝불' }],
  일식: [{ name: '초밥' }, { name: '텐동' }],
};

foodByCategory['양식']; //Food[] 로 추론
foodByCategory['양식'].map((food) => console.log(food.name)); // Uncaught TypeError

 

 

이때 옵셔널 체이닝으로 런타임 에러를 방지할 수 있지만, 개발자가 일일이 undefined지 확인하고 붙여야하는 번거러움이 생긴다.

foodByCategory['양식']?.map((food) => console.log(food.name));

 

이를 해결하기 위한 방법은 2가지이다.

 

  1. 유닛 타입으로 변경하기
    type UnitTypeCategory = '한식' | '일식';​​
    하지만 키가 무한해야할 상황에는 적합하지 않다.
  2. Partial을 활용해서 정확한 타입 표기
    Partial을 사용하여 해당 값이 undefined일 수 있는 상태임을 표시할 수 있다.
    type FoodCategory = string;
    
    type PartialRecord<K extends string, T> = Partial<Record<K, T>>;
    
    interface Food2 {
      name: string;
    }
    
    const foodByCategory2: PartialRecord<FoodCategory, Food[]> = {
      한식: [{ name: '제육' }, { name: '뚝불' }],
      일식: [{ name: '초밥' }, { name: '텐동' }],
    };
    
    foodByCategory2['양식'].map((food) => console.log(food.name));// Obeject is possibley 'undefined'​
     Partial을 사용할 경우 모든 key값은 optional이 되므로 undefined일 수 있다는 경고가 나와서 옵셔널 체이닝이나 조건문을 개발자가 쓰도록 유도할 수 있다.


 

 

 

+ Recent posts