iBetter Books
수정

Ch 04. 조건부 타입과 infer

조건부 타입은 TypeScript의 타입 시스템에서 if-else와 같은 역할을 합니다. 타입이 특정 조건을 만족하는지에 따라 다른 타입을 반환합니다. infer는 조건부 타입 안에서 타입을 추출하는 데 사용됩니다. 이 두 기능을 조합하면 타입 수준의 프로그래밍이 가능합니다.

조건부 타입 기본 문법

// 파일: src/generics/conditional-basic.ts// T extends U ? X : Y// T가 U에 할당 가능하면 X, 아니면 Ytype IsString<T> = T extends string ? "yes" : "no";type A = IsString<string>;   // "yes"type B = IsString<number>;   // "no"type C = IsString<"hello">;  // "yes" — string 리터럴도 string에 할당 가능// 실용적인 예시type NonNullable2<T> = T extends null | undefined ? never : T;type D = NonNullable2<string | null>;  // stringtype E = NonNullable2<number | undefined>; // number

분배 법칙 (Distributive Conditional Types)

조건부 타입이 유니온 타입에 적용될 때 각 멤버에 개별적으로 적용됩니다.

// 파일: src/generics/distributive.tstype ToArray<T> = T extends unknown ? T[] : never;// T = string | number에 적용되면// (string extends unknown ? string[] : never) | (number extends unknown ? number[] : never)// = string[] | number[]type F = ToArray<string | number>; // string[] | number[]// 분배를 막으려면 튜플로 감싸기type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;type G = ToArrayNonDist<string | number>; // (string | number)[]// 유용한 분배 타입들type Flatten<T> = T extends Array<infer Item> ? Item : T;type H = Flatten<string[]>;          // stringtype I = Flatten<number[][]>;        // number[]type J = Flatten<string>;            // string (배열이 아니면 그대로)

infer로 타입 추출하기

infer는 조건부 타입 내에서 타입을 "추출"(캡처)하는 키워드입니다. extends 절 안에서만 사용할 수 있습니다.

// 파일: src/generics/infer-basic.ts// 함수의 반환 타입 추출type ReturnType2<T> = T extends (...args: any[]) => infer R ? R : never;function greet(name: string): string {  return `Hello, ${name}`;}function getUser(id: number): { id: number; name: string } {  return { id, name: "Alice" };}type GreetReturn = ReturnType2<typeof greet>;   // stringtype UserReturn = ReturnType2<typeof getUser>;  // { id: number; name: string }// 함수의 첫 번째 매개변수 타입 추출type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;type P1 = FirstParam<typeof greet>;   // stringtype P2 = FirstParam<typeof getUser>; // number// 함수의 모든 매개변수 타입 추출type Parameters2<T> = T extends (...args: infer P) => any ? P : never;function createUser(name: string, age: number, email: string): void {}type CreateUserParams = Parameters2<typeof createUser>; // [string, number, string]

Promise 내부 타입 추출하기

// 파일: src/generics/infer-promise.ts// Promise를 풀어서 내부 타입 얻기type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;type R1 = Awaited2<Promise<string>>;           // stringtype R2 = Awaited2<Promise<Promise<number>>>;  // number// 실용적인 활용 — async 함수 반환 타입async function fetchUser(id: number) {  return { id, name: "Alice", email: "[email protected]" };}type FetchUserResult = Awaited2<ReturnType<typeof fetchUser>>;// { id: number; name: string; email: string }

배열 요소 타입 추출하기

// 파일: src/generics/infer-array.ts// 배열의 요소 타입 추출type ElementType<T> = T extends (infer E)[] ? E : never;type S1 = ElementType<string[]>;          // stringtype S2 = ElementType<[number, string]>;  // number | stringtype S3 = ElementType<never[]>;           // never// 중첩 배열 평탄화type DeepFlatten<T> = T extends (infer E)[]  ? E extends unknown[]    ? DeepFlatten<E>    : E  : T;type Nested = [[1, 2], [3, [4, 5]]];type Flat = DeepFlatten<number[][][]>; // number

실전 예제: API 클라이언트 타입 추론

조건부 타입과 infer를 조합해 실용적인 타입 유틸리티를 만듭니다.

// 파일: src/generics/api-types.ts// 함수 타입에서 비동기 결과 추출type AsyncReturnType<T extends (...args: any) => Promise<any>> =  T extends (...args: any) => Promise<infer R> ? R : never;// API 함수들async function listUsers(): Promise<{ id: number; name: string }[]> {  return [];}async function getUser(id: number): Promise<{ id: number; name: string; email: string }> {  return { id, name: "", email: "" };}type UserList = AsyncReturnType<typeof listUsers>;// { id: number; name: string }[]type UserDetail = AsyncReturnType<typeof getUser>;// { id: number; name: string; email: string }// 생성자 파라미터 추출type ConstructorParameters2<T extends new (...args: any) => any> =  T extends new (...args: infer P) => any ? P : never;class UserService {  constructor(    private readonly apiUrl: string,    private readonly timeout: number  ) {}}type UserServiceParams = ConstructorParameters2<typeof UserService>;// [string, number]const params: UserServiceParams = ["https://api.example.com", 3000];const service = new UserService(...params);

조건부 타입으로 타입 필터링

// 파일: src/generics/type-filter.ts// 특정 타입만 추출하는 유틸리티type Extract2<T, U> = T extends U ? T : never;type Exclude2<T, U> = T extends U ? never : T;type Animals = "cat" | "dog" | "bird" | "fish";type Pets = Extract2<Animals, "cat" | "dog">;     // "cat" | "dog"type Wild = Exclude2<Animals, "cat" | "dog">;     // "bird" | "fish"// 객체에서 특정 타입의 값을 가진 키만 추출type KeysWithValueType<T, V> = {  [K in keyof T]: T[K] extends V ? K : never;}[keyof T];interface Profile {  id: number;  name: string;  email: string;  age: number;  isActive: boolean;}type StringKeys = KeysWithValueType<Profile, string>;// "name" | "email"type NumberKeys = KeysWithValueType<Profile, number>;// "id" | "age"

조건부 타입과 infer는 TypeScript의 타입 수준 프로그래밍을 가능하게 합니다. 처음에는 낯설어 보이지만, 라이브러리 타입 정의를 읽거나 복잡한 타입 유틸리티를 만들 때 반드시 마주치는 패턴입니다. TypeScript의 내장 유틸리티 타입인 ReturnType, Parameters, Awaited, Extract, Exclude 모두 이 기법으로 구현되어 있습니다.