유틸리티 타입을 공부해보자!

타입스크립트의 유틸리티 타입이라는 것을 들어보고, 공부하기도 했었지만

결국 이를 프로젝트에서 사용하지 않아 익숙해지지 않았고 다시 까먹게 되어 자주 쓰시는 것 같은 유틸리티 타입을 분석해보고자 합니다.

이를 분석하면 결국 맵드 타입, 인덱스 시그니처, 타입 추론 등과 같은 고급 스킬들을 더 잘 이해할 수 있을거라 생각하였습니다!

 

"12 Must-Have TypeScript Utility Types with Uses and Example" 글을 읽고 제 분석을 추가한 글입니다.

 

목차는 다음과 같습니다.

1. Object Manipulation Types

  • `Partial<T>`
  • `Required<T>`
  • `Readonly<T>`
  • `Pick<T, Keys>`
  • `Record<Keys, Value>`
  • `Omit<T, Keys>`
  • `Mutable<T>`

2. Union Manipulation Types

  • `Exclude<T, Excluded>`
  • `NonNullable<T>`
  • `Extract<T, Extracted>`

3. Function Manipulation Types

  • `Parameters<Function>`
  • `ReturnType<Function>`
  • `Awaited<Function>`

Object Manipulation Types

1. Partial<T>

`Partial`은 제네릭으로 들어온 `T` 타입의 모든 속성을 옵셔널로 만듭니다.

`Partial`을 구현하면 다음과 같습니다.

type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

T라는 객체의 모든 키들의 속성에 `?`를 붙임으로써 옵셔널하게 만드는 것이죠

 

2. Required<T>

`Required`는 반대로 제네릭으로 들어온 `T`의 모든 속성을 필수로 만들어 버립니다

type MyRequired<T> = {
  [P in keyof T]-?: T[P];
};

T라는 객체의 모든 키들의 속성에 `-?`를 붙임으로써 옵셔널한 속성을 필수로 만들어 버립니다.

 

3. Readonly<T>

`Readonly`는 이름에서도 유추할 수 있듯이 제네릭으로 들어온 `T`의 모든 속성에 readonly라는 불변성 옵션을 추가해줍니다.

type MyReadonly<T> = {
  readonly [P in keyof T]: T[P];
};

 

4. Pick<T>

`Pick`은 객체의 특정 속성만을 추출한 타입을 만들어낼 수 있습니다.

type MyPick<T, K extends keyof T> = {
  [key in K]: T[key];
};

 

두번째 제네릭이 첫 번째 제네릭인 `T`의 속성이어야 한다는 조건을 주어 `T`라는 타입의 키여야 함을 강제했습니다.

 

5. Record<T>

`Record`는 `K` 타입을 키로, `V` 타입을 값의 타입으로 하는 새로운 타입을 만들어냅니다.

// type MyRecord<K extends keyof any, V> = {
type MyRecord<K extends symbol | string | number, V> = {
  [key in K]: V;
};

 

만약 `MyRecord<'a'|'b', string>`이면 a, b 속성을 가지고 있고 둘 다 string 타입인 새로운 타입을 만들어 내는 셈이죠

Record<'a'|'b', string> 
// 위의 타입은 아래와 같습니다!
{
  a: string
  b: string
}

6. Omit<T>

`Omit`은 객체의 특정 속성을 제외한 타입을 만들어냅니다! 

type MyOmit<T, K extends keyof T> = {
  [key in keyof T extends K ? never : keyof T]: T[key];
};

 

`keyof T extends K ? never : keyof T`라는 문법이 생소하실 수도 있으실 텐데

`A extends B ? C : D`는 A가 B에 속하면 C 아니면 D이다는 타입을 나타냅니다.

즉, `keyof T`가 `K`에 속하면 `never` 아니면 `keyof T`를 리턴한다고 생각하시면 됩니다. 

 

이는 타입스크립트가 제공해주는 `Exclude`라는 유틸리티 타입을 통해 더 선언적인 타입을 구현할 수 있게 됩니다.

type MyOmit<T, K extends keyof T> = {
  [key in Exclude<keyof T, K>]: T[key];
};

 

7. Mutable<T>

`Mutable`은 객체의 불변성을 가변성으로 만들어주기 위해 `readonly` 옵션을 제거해주는 유틸리티 타입입니다.

type MyMutable<T> = {
  -readonly [P in keyof T]: T[P];
};

Union Manipulation Types

이 타입을 직접 만들 때는 조건부 타입을 굉장히 많이 쓰게 됩니다. 

1. Exclude

`Exclude` 타입은 유니온 타입에서 특정 타입을 제외한 나머지 타입을 만들어주는 유틸리티 타입입니다.

type MyExclude<T, K> = T extends K ? never : T;

`T`에 `K`가 포함되면 never를 반환하고 포함되지 않는 경우`T`를 반환합니다. 

내부적으로 유니언을 순회하면서 체크하는 형식일 것 같습니다.

2. NonNullable

`NonNullable`은 타입에서 `null`과 `undefined`를 제외시켜주는 유틸리티 타입입니다.

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

3. Extract

`Extract`타입은 반대로 유니온 타입에서 특정 타입만을 추출한 타입을 만들어주는 유틸리티 타입입니다.

type MyExtract<T, U> = T extends U ? T : never;

Function Manipulation Types

이 타입을 실제로 구현하게 된다면 추론 타입을 많이 사용하게 됩니다. 명시적으로 타입을 지정하는 것이 아닌 함수의 매개변수, 반환값을 기반으로 타입을 유추해야하기 때문입니다.

1. Parameters

함수를 제네릭 인자로 받아 매개변수의 타입을 추론하도록 도와주는 유틸리티 타입입니다.

type MyParameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;

2. ReturnType

함수를 제네릭 인자로 받아 반환값의 타입을 추론하도록 도와주는 유틸리티 타입입니다.

type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : never;

3. Awaited

`Promise`가 `resolve`해주는 타입을 추론하도록 도와주는 유틸리티 타입이며,

`Promise`이외의 타입을 넣게 되면 해당 타입을 반환해줍니다. 

 

`Awaited<Promise<string> | number>`는 `string | number`으로 변환됩니다.

type MyAwaited<T> = T extends null | undefined
  ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
  : T extends object & { then(onfulfilled: infer F, ...args: infer _): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
    ? F extends (value: infer V, ...args: infer _) => any // if the argument to `then` is callable, extracts the first argument
      ? MyAwaited<V> // recursively unwrap the value
      : never // the argument to `then` was not callable
    : T; // non-object or non-thenable

타입이 굉장히 복잡한데 하나씩 해석해보도록 하겠습니다.

 

1. 타입이 null 이나 undefined인 경우 null, undefined 타입을 반환합니다.

 

2. 타입이 객체이고 then 메서드를 가지고 있는 경우와 아닌 경우를 분기처리합니다.

객체가 아닌 경우, 객체인데 `then` 메서드가 없는 경우는 해당 타입을 반환합니다.

 

3. then의 onfulfilled 함수가 있는지 없는지에 대한 분기처리합니다.

첫 번째 매개변수를 추론하고 이 매개변수를 기반으로 `Promise`가 없을 때까지 재귀적으로 `Awaited` 유틸리티 타입을 호출합니다.

 

이렇게 자주 쓰이는 12개의 유틸리티 타입을 분석해보았는데, 실제 프로젝트에서도 지속적으로 사용해서 완전히 익혀야겠네요..!