Javascript

TypeScript 개발 필수템! 유틸리티 타입 20가지 완벽 분석

드리프트2 2024. 8. 27. 21:42

 

TypeScript에서 제공하는 유틸리티 타입(Utility Types)에 대해 모두 알아보겠습니다.

 

정보 출처는 TypeScript 공식 사이트의 유틸리티 타입 페이지를 기본으로 하며, 추가적인 설명을 덧붙였습니다.

 

https://www.typescriptlang.org/docs/handbook/utility-types.html

 

Documentation - Utility Types

Types which are globally included in TypeScript

www.typescriptlang.org

 

자주 사용하는 유틸리티 타입부터 덜 사용하는 유틸리티 타입 순서로 정리했습니다.

 

Pick<Type, Keys>

추가된 버전: 2.1

Type에서 Keys 프로퍼티의 집합을 선택하여 새로운 타입을 생성합니다.

 

즉, Type에서 특정 프로퍼티만 뽑아낸 타입을 만들 수 있습니다.

 

Keys는 단순 문자열 리터럴 또는 문자열 리터럴의 유니온을 지정할 수 있습니다.

type T = {
  a: number;
  b: boolean;
  c: string;
};

// 타입 T에서 프로퍼티 a와 c만 추출한 타입 AC를 생성
type AC = Pick<T, "a" | "c">;
// type AC = {
//   a: number;
//   c: string;
// }

const x: AC = { a: 42, c: "세계" };

 

Pick의 장점은 타입을 그대로 재활용하기 때문에 변경 사항이 자동으로 반영된다는 것입니다.

 

예를 들어 위 예시에서 T의 프로퍼티 a의 타입을 string으로 변경하면, 타입 AC의 a 타입도 자동으로 string으로 변경됩니다.

 

따라서 변경 시 수정해야 할 부분이 줄어들어 코드 변경이 쉬워지고, 타입이 맞지 않으면 에러가 발생하여 실행 전에 문제를 파악할 수 있습니다.

type T = {
  a: string; // number에서 string으로 변경
  b: boolean;
  c: string;
};

type AC = Pick<T, "a" | "c">;
// type AC = {
//   a: string; // Pick했기 때문에 원래 타입의 변경 사항이 자동으로 반영됩니다.
//   c: string;
// }

// !! ERROR !!
// 타입을 사용하는 곳에서 일관성이 맞지 않으면 에러가 발생합니다.
const x: AC = { a: 42, c: "세계" }; // 타입 'number'를 타입 'string'에 할당할 수 없습니다.

 

또한, 프로퍼티에 존재하지 않는 Keys를 지정하면 에러가 발생하는 것도 장점입니다.

 

Omit<Type, Keys>

추가된 버전: 3.5

Type에서 Keys (문자열 또는 문자열의 유니온 타입)를 제거한 타입을 생성합니다. Pick의 반대입니다.

type T = {
  a: number;
  b: boolean;
  c: string;
};

// 타입 T에서 프로퍼티 a와 c를 제거한 타입을 생성
type OnlyB = Omit<T, "a" | "c">;
// type OnlyB = {
//   b: boolean;
// };

const x: OnlyB = { b: true };

 

Omit은 Keys의 제약이 느슨하여 Type에 존재하지 않는 프로퍼티를 Keys로 지정해도 에러가 발생하지 않으므로, 타입 오류에 주의해야 합니다.

 

Pick에서는 존재하지 않는 키를 지정하면 에러가 발생하는데, Omit도 마찬가지로 에러를 발생시켜야 한다는 요구가 있었지만 TypeScript 팀은 이를 거부했습니다.

type T = {
  a: number;
  b: boolean;
  c: string;
};

// 타입 T에 존재하지 않는 프로퍼티를 Omit으로 지정해도 에러가 발생하지 않습니다.
type U = Omit<T, "z">;
// type U = {
//   a: number;
//   b: boolean;
//   c: string;
// };

Exclude<UnionType, ExcludedMembers>

추가된 버전: 2.8

유니온 타입 UnionType에서 ExcludedMembers에 할당 가능한 타입을 제외한 타입을 생성합니다.

 

즉, UnionType과 ExcludedMembers의 차집합을 구할 수 있습니다. (원래는 해당 동작 그대로 Diff라는 이름으로 제안되었었다고 합니다.)

 

더 간단히 말하면 유니온 타입에 대한 Omit입니다.

// "a"를 제거
type T0 = Exclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c";

// "a"와 "b"를 제거
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// type T1 = "c";

// 함수를 제거
type T2 = Exclude<string | number | (() => void), Function>;
// type T2 = string | number;

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

// 타입 Shape에서 { kind: "circle" }을 제외
type T3 = Exclude<Shape, { kind: "circle" }>;
// type T3 =
//   | {
//       kind: "square";
//       x: number;
//     }
//   | {
//       kind: "triangle";
//       x: number;
//       y: number;
//     };

Extract<Type, Union>

추가된 버전: 2.8

Type에서 Union에 할당 가능한 모든 유니온 타입의 멤버를 추출한 타입을 생성합니다.

 

즉, 유니온 타입에서 Type 중 Union에 존재하는 같은 타입만 추출한다는 것입니다 (엄밀히 말하면 조금 다릅니다).


이를 더 간략하게 말하면 Type과 Union의 AND (논리곱)을 구하는 것과 거의 같다고 할 수 있습니다.

// 공통된 "a"만 추출
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
// type T0 = "a";

// 함수만 추출
type T1 = Extract<string | number | (() => void), Function>;
// type T1 = () => void;

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

// { kind: "circle" }에 할당 가능한 타입만 추출
type T2 = Extract<Shape, { kind: "circle" }>;
// type T2 = {
//   kind: "circle";
//   radius: number;
// };

Extract의 반대 기능

NonNullable

추가된 버전: 2.8

Type에서 null과 undefined를 제거한 타입을 생성합니다.

type T0 = NonNullable<string | number | undefined>;
// type T0 = string | number;

type T1 = NonNullable<string[] | null | undefined>;
// type T1 = string[];

Partial

추가된 버전: 2.1

Type의 모든 프로퍼티를 선택적으로 만든 타입을 생성합니다.

type A = {
  n: number;
  s: string | null;
};

// type A의 모든 프로퍼티가 선택적이 된 타입을 얻습니다.
type B = Partial<A>;
// type B = {
//       n?: number | undefined;
//       s?: string | null | undefined;
// }

 

Partial은 객체의 일부 값을 덮어쓰는 함수를 만들 때 유용합니다.


이는 테스트 데이터 생성 시 일부 프로퍼티를 변경하는 경우 등에 자주 사용됩니다.

type A = {
  n: number;
  s: string;
};

// 타입 A의 객체 x를 객체 y로 덮어쓴 새로운 객체를 생성하는 함수.
// y는 타입 A의 Partial이므로 모든 프로퍼티를 지정할 필요는 없습니다.
function update(x: A, y: Partial<A>): A {
  return { ...x, ...y };
}

const source: A = {
  n: 1,
  s: "원본",
};

const v1 = update(source, { n: 7, s: "변경" });
// { n: 7, s: '변경' }

// 일부 프로퍼티만 (이 경우 n만) 덮어쓸 수 있습니다.
const v2 = update(source, { n: 42 });
// { n: 42, s: '원본' }

// 아무것도 덮어쓰지 않을 수도 있습니다.
const v3 = update(source, {});
// { n: 1, s: '원본' }

Required

추가된 버전: 2.8

Type의 모든 프로퍼티를 필수로 만든 타입을 생성합니다. Partial의 반대입니다.

type A = {
  n?: number;
  s?: string;
};

// type A의 모든 프로퍼티가 필수(선택적이지 않음)가 된 타입을 얻습니다.
type B = Required<A>;
// type B = {
//   n: number;
//   s: string;
// }

const x: B = {
  n: 0,
  s: "문자열",
};

// !! ERROR !!
// s가 부족하기 때문에 에러 발생
const y: B = {
  n: 0,
};

 

프로퍼티가 선택적(?)이라는 것과 undefined를 유니온 타입으로 허용하는 것(| undefined)은 의미가 다르기 때문에, Required에서는 유니온의 undefined는 변경되지 않습니다.

type C = {
  a?: number;
  b: number | undefined;
};

// Required에 의해 a는 선택적이지 않게 되지만, b는 undefined도 할당 가능한 상태로 유지됩니다.
type D = Required<C>;
// type D = {
//   a: number;
//   b: number | undefined;
// }

// 모든 값을 입력했기 때문에 OK
const x: D = { a: 0, b: 1 };

// b는 undefined를 할당 가능
const y: D = { a: 0, b: undefined };

// !! ERROR !!
// b는 선택적이지 않아 생략할 수 없기 때문에 에러 발생
const z: D = { a: 0 }; // ERROR!!

 

그리고 조금 복잡하지만, 선택적 ?와 | undefined를 동시에 지정한 타입에 Required를 적용하면, 필수가 되면서 undefined를 허용하지 않게 됩니다.

type E = {
  a?: number | undefined;
};

// 선택적 + undefined 허용을 Required하면, 필수가 되고 undefined를 허용하지 않게 됩니다.
type F = Required<E>;
// type F = {
//   a: number;
// }

const x: F = { a: 42 };

// !! ERROR !!
// a는 생략 불가능하여 에러 발생
const y: F = {}; // ERROR!!

// !! ERROR !!
// a는 undefined를 허용하지 않게 되어 에러 발생
const z: F = { a: undefined }; // ERROR!!

Readonly

추가된 버전: 2.1

Type의 모든 프로퍼티를 readonly로 설정한 타입을 생성합니다. 즉, 프로퍼티가 모두 읽기 전용이 되어 값을 할당할 수 없게 됩니다.

type A = {
  n: number;
};

// 타입 A를 읽기 전용으로 만든 변수 x를 선언
const x: Readonly<A> = {
  n: 0,
};

// !! ERROR !!
x.n = 42; // 읽기 전용 프로퍼티이므로 'n'에 할당할 수 없습니다.

 

하지만 배열이나 객체는 readonly가 되어도 내부는 조작 가능하기 때문에 완전히 읽기 전용 타입이 되는 것은 아닙니다.

type B = {
  a: string[];
};

const x: Readonly<B> = {
  a: ["아", "이"],
};

// readonly 배열이나 객체는 조작 가능
x.a.push("우"); // [ '아', '이', '우' ]

// !! ERROR !!
// 하지만 값을 할당할 수는 없습니다.
x.a = ["응"]; // 읽기 전용 프로퍼티이므로 'a'에 할당할 수 없습니다.

Record<Keys, Type>

추가된 버전: 2.1

프로퍼티 키가 Keys 타입이고, 프로퍼티 타입이 Type인 객체 타입을 생성합니다.


이는 특정 타입의 프로퍼티를 다른 타입으로 매핑하는 데 사용할 수 있습니다.

type CatName = "tama" | "mike" | "kuro";

type CatInfo = {
  age: number;
  breed: string;
};

// 고양이 데이터를 나타내는 타입
// 프로퍼티 키가 CatName 타입이고, 값이 CatInfo 타입
type Cats = Record<CatName, CatInfo>;

const cats: Cats = {
  tama: { age: 5, breed: "아메리칸 숏헤어" },
  mike: { age: 5, breed: "잡종" },
  kuro: { age: 5, breed: "봄베이" },
};
// 만약 CatName을 모두 포함하지 않은 경우 (예를 들어 kuro가 빠진 경우) 타입 에러가 발생합니다.

Parameters

추가된 버전: 3.1

Type 함수 타입의 인수 타입의 튜플 타입을 생성합니다.

 

즉, 함수의 인수 타입을 얻을 수 있습니다.

// 인수가 없으므로 빈 튜플 타입
type T0 = Parameters<() => string>;
// type T0 = [];

// 인수가 문자열 1개이므로 해당 튜플 타입
type T1 = Parameters<(s: string) => void>;
// type T1 = [s: string];

// 제네릭 타입일 때는 확정되지 않으므로 unknown
type T2 = Parameters<<T>(arg: T) => T>;
// type T2 = [arg: unknown];

function f1(arg: { a: number; b: string }): void {}

// 함수에 적용할 때는 typeof로 함수를 타입으로 변환
type T3 = Parameters<typeof f1>;
// type T3 = [
//   arg: {
//     a: number;
//     b: string;
//   }
// ];

// any는 모든 타입 허용
type T4 = Parameters<any>;
// type T4 = unknown[];

// never는 아무것도 없음
type T5 = Parameters<never>;
// type T5 = never;

// !! ERROR !!
// string 타입은 함수가 아니므로 에러 발생
type T6 = Parameters<string>; // 타입 'string'은 제약 '(...args: any) => any'를 충족하지 않습니다.
// type T6 = never;

// !! ERROR !!
// 타입 Function은 너무 모호하여 에러가 발생하는 것으로 보입니다.
type T7 = Parameters<Function>;
// 타입 'Function'은 제약 '(...args: any) => any'를 충족하지 않습니다.
//   타입 'Function'에는 시그니처 '(...args: any): any'에 일치하는 항목이 없습니다.
// type T7 = never;

 

이는 React에서 컴포넌트의 프로퍼티 타입을 외부에서 얻을 때 자주 사용됩니다.


프로퍼티 타입이 export되지 않아도 외부에서 알 수 있습니다.

 

ReturnType

추가된 버전: 2.8

함수 Type이 반환하는 타입을 얻습니다.

declare function f1(): { a: number; b: string };

type T0 = ReturnType<() => string>;
// type T0 = string;

type T1 = ReturnType<(s: string) => void>;
// type T1 = void;

// 단순 제네릭의 경우 알 수 없음
type T2 = ReturnType<<T>() => T>;
// type T2 = unknown;

// 제네릭에서 타입이 미리 알려진 범위까지는 좁혀집니다.
type T3 = ReturnType<<T extends U, U extends number[]>() => T>;
// type T3 = number[];

// 함수 자체에 적용할 경우 typeof로 타입으로 변환
type T4 = ReturnType<typeof f1>;
// type T4 = {
//   a: number;
//   b: string;
// };

type T5 = ReturnType<any>;
// type T5 = any;

type T6 = ReturnType<never>;
// type T6 = never;

// !! ERROR !!
// string은 함수가 아닙니다.
type T7 = ReturnType<string>; // 타입 'string'은 제약 '(...args: any) => any'를 충족하지 않습니다.
// type T7 = any;

// !! ERROR !!
type T8 = ReturnType<Function>;
// 타입 'Function'은 제약 '(...args: any) => any'를 충족하지 않습니다.
//   타입 'Function'에는 시그니처 '(...args: any): any'에 일치하는 항목이 없습니다.
// type T8 = any;

Awaited

추가된 버전: 4.5

비동기 함수의 await 또는 Promise의 .then()과 같은 작업을 수행합니다.


특히 Promise의 재귀적인 타입 추출을 모델링합니다.

// Promise를 제외한 타입을 추출할 수 있습니다.
type A = Awaited<Promise<string>>;
// type A = string

// Promise가 중첩되어 이중으로 되어 있어도 재귀적으로 타입을 추출할 수 있습니다.
type B = Awaited<Promise<Promise<number>>>;
// type B = number

// Promise와 그 외의 타입이 함께 있어도 괜찮습니다.
type C = Awaited<boolean | Promise<number>>;
// type C = number | boolean

 

도입 전에는 Promise.all이나 Promise.race 등의 타입 추론에서 문제가 발생하는 경우가 있었지만, 이를 해결했습니다.

 

NoInfer

추가된 버전: 5.4

포함된 타입에 대한 추론을 차단합니다.


그 외에는 NoInfer은 Type과 동일합니다.

 

간단히 말하면 TypeScript가 수행하는 타입 추론(type inference)에 불필요한 타입이 추가되지 않도록 차단하는 기능입니다.

 

예를 들어 아래 코드에서 추론된 타입에 불필요한 타입이 포함되어 에러가 발생해야 하는 부분에서 에러가 발생하지 않습니다.

// 문자열 리스트와 리스트의 기본값을 설정하는 함수
// 리스트에 없는 값이 기본값으로 지정되었을 때 타입 에러를 발생시키고 싶습니다.
function fn<C extends string>(values: C[], defaultValue: C) {
  // ...
}

// 기본값을 설정하는 일반적인 사용법
fn(["a", "b", "c"], "a");

// 리스트에 없는 문자열을 기본값으로 설정해도 에러가 발생하지 않습니다!
// 이는 TypeScript가 C의 타입으로 "z"도 포함하는 타입 추론을 수행하여,
// 타입 C가 "a" | "b" | "c" | "z"로 타입 추론되기 때문입니다.
fn(["a", "b", "c"], "z");

 

여기서 NoInfer를 사용하면 특정 타입을 추론 대상에서 제외할 수 있으므로, 의도한 대로 동작하도록 할 수 있습니다.

// defaultValue의 타입을 NoInfer로 감싸서 타입 추론 대상에서 제외합니다.
function fn<C extends string>(values: C[], defaultValue: NoInfer<C>) {
  // ...
}

// 일반적으로 기본값을 설정할 수 있습니다.
fn(["a", "b", "c"], "a");

// !! ERROR !!
// 타입 C가 "a" | "b" | "c"로 타입 추론되고 "z"가 포함되지 않으므로, 정상적으로 에러가 발생합니다.
fn(["a", "b", "c"], "z"); // 타입 '"z"'의 인수를 타입 '"a" | "b" | "c"'의 매개변수에 할당할 수 없습니다.

 

 


 

 

아래는 사용 빈도가 낮은 유틸리티 타입입니다.

ConstructorParameters

추가된 버전: 3.1

Parameters의 생성자 함수 타입 버전입니다.

 

최근 TypeScript에서는 class가 거의 사용되지 않기 때문에, 접할 기회는 거의 없습니다.

type T0 = ConstructorParameters<ErrorConstructor>;
// type T0 = [message?: string, options?: ErrorOptions]

type T1 = ConstructorParameters<FunctionConstructor>;
// type T1 = string[];

type T2 = ConstructorParameters<RegExpConstructor>;
// type T2 = [pattern: string | RegExp, flags?: string];

class C {
  constructor(a: number, b: string) {}
}

type T3 = ConstructorParameters<typeof C>;
// type T3 = [a: number, b: string];

type T4 = ConstructorParameters<any>;
// type T4 = unknown[];

// !! ERROR !!
type T5 = ConstructorParameters<Function>;
// 타입 'Function'은 제약 'abstract new (...args: any) => any'를 충족하지 않습니다.
//   타입 'Function'에는 시그니처 'new (...args: any): any'에 일치하는 항목이 없습니다.

InstanceType

추가된 버전: 2.8

생성자 함수 Type의 인스턴스 타입으로 구성된 타입을 생성합니다.

 

즉, 인스턴스의 클래스를 얻을 수 있지만, 최근에는 class가 거의 사용되지 않으므로 잘 보이지 않습니다.

class C {
  x = 0;
  y = 0;
}

type T0 = InstanceType<typeof C>;
// type T0 = C;

type T1 = InstanceType<any>;
// type T1 = any;

type T2 = InstanceType<never>;
// type T2 = never;

// !! ERROR !!
type T3 = InstanceType<string>; // 타입 'string'은 제약 'abstract new (...args: any) => any'를 충족하지 않습니다.
// type T3 = any;

// !! ERROR !!
type T4 = InstanceType<Function>;
// 타입 'Function'은 제약 'abstract new (...args: any) => any'를 충족하지 않습니다.
//   타입 'Function'에는 시그니처 'new (...args: any): any'에 일치하는 항목이 없습니다.
// type T4 = any;

ThisParameterType

추가된 버전: 3.3

함수 타입 Type의 this 인수 타입을 추출합니다.


함수 타입이 this 인수를 갖지 않으면 unknown이 됩니다.

 

거의 사용하지 않습니다.

function toHex(this: Number) {
  return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

OmitThisParameter

추가된 버전: 3.3

함수 타입 Type의 this 인수 타입을 제거한 타입을 생성합니다.

 

거의 사용하지 않습니다.

function toHex(this: Number) {
  return this.toString(16);
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

console.log(fiveToHex()); // 5

ThisType

추가된 버전: 2.3

함수 내에서 this의 타입을 Type으로 처리할 수 있도록 합니다.


noImplicitThis 플래그가 활성화되어 있어야 합니다.

 

거의 사용하지 않습니다.