TypeScript 제네릭 조건, 매핑 및 추론 마스터하기
Grace Collins
Solutions Engineer · Leapcell

기본 제네릭을 넘어 TypeScript의 잠재력 발휘
TypeScript의 유형 시스템은 믿을 수 없을 정도로 강력하며, 제네릭 기능은 유연하고 재사용 가능한 코드를 구축하는 초석입니다. 기본 제네릭을 사용하면 다양한 데이터 유형으로 작동하는 함수와 클래스를 작성할 수 있지만, 조건부 유형, 매핑된 유형 및 infer
키워드와 같은 고급 기능에 대해 자세히 알아볼 때 진정한 마법이 펼쳐집니다. 이러한 구조는 입력에 따라 동적으로 조정되는 유형을 정의할 수 있도록 하여 매우 강력한 표현력 있고 유지 관리 가능한 코드베이스를 만들 수 있습니다. 유형 안전성과 코드 품질이 가장 중요한 점점 더 복잡해지는 웹 개발 환경에서 이러한 고급 제네릭 기술을 마스터하는 것은 더 이상 사치가 아니라 진지한 TypeScript 개발자에게는 필수입니다. 이 기사에서는 이러한 강력한 기능을 명확하게 설명하고 실제로 적용하는 예를 보여주며, 이를 최대한 활용할 수 있도록 안내할 것입니다.
고급 제네릭 유형 언패킹
조건부 유형, 매핑된 유형 및 infer
의 복잡성을 자세히 살펴보기 전에 TypeScript에서 유형과 제네릭이 근본적으로 무엇을 나타내는지에 대한 이해를 빠르게 확고히 해 봅시다. 본질적으로 유형은 값의 모양과 동작을 설명합니다. 반면에 제네릭은 유형 안전성을 희생하지 않고 유연성을 제공하면서 모든 유형으로 작동할 수 있는 함수, 클래스 및 인터페이스를 작성할 수 있도록 하는 유형 변수와 같습니다. 이제 고급 개념을 탐색해 보겠습니다.
조건부 유형
조건부 유형은 유형 관계를 평가하는 조건에 따라 두 가지 다른 유형 중에서 선택할 수 있도록 합니다. 주로 extends
키워드를 사용하며 JavaScript 삼항 연산자와 유사한 구문을 사용합니다: SomeType extends OtherType ? TrueType : FalseType
.
원칙: 핵심 아이디어는 유형 수준 검사를 수행하는 것입니다. SomeType
이 OtherType
에 할당 가능한 경우(즉, SomeType
이 OtherType
의 하위 유형이거나 동일한 경우) TrueType
이 선택됩니다. 그렇지 않으면 FalseType
이 선택됩니다.
구현 및 적용: 함수의 반환 유형을 추출하고 싶지만 입력이 실제로 함수인 경우에만 해당하는 시나리오를 생각해 보세요.
type NonFunction = string | number | boolean; type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : NonFunction; // 예시 사용법: function sum(a: number, b: number): number { return a + b; } type SumReturnType = GetReturnType<typeof sum>; // number const myString = "hello"; type StringReturnType = GetReturnType<typeof myString>; // NonFunction (typeof myString은 함수가 아니기 때문) function greet(name: string): void { console.log(`Hello, ${name}`); } type GreetReturnType = GetReturnType<typeof greet>; // void
이 예에서 GetReturnType<T>
는 조건부 유형입니다. T
가 (...args: any[]) => infer R
를 확장하는지 확인합니다. T
가 함수 유형인 경우(다음으로 논의할) infer R
키워드는 해당 반환 유형을 캡처하고 R
이 선택됩니다. 그렇지 않은 경우 NonFunction
이 선택됩니다. 이는 다른 유형에서 작동하는 유형 안전 유틸리티 함수를 작성하는 데 매우 유용합니다.
또 다른 일반적인 사용 사례는 유형 필터링입니다.
type FilterString<T> = T extends string ? T : never; type MixedTuple = [1, "hello", true, "world"]; type OnlyStringsFromTuple = FilterString<MixedTuple[number]>; // "hello" | "world"
여기서 MixedTuple[number]
는 1 | "hello" | true | "world"
의 유니온을 생성합니다. 그런 다음 FilterString
은 유니온의 각 유형을 반복하고 string
유형만 조건을 통과하면 "hello" | "world"
가 결과로 나옵니다.
매핑된 유형
매핑된 유형은 기존 객체 유형의 속성을 새 객체 유형으로 변환할 수 있도록 합니다. 본질적으로 주어진 유형의 키를 반복하고 각 속성 유형에 변환을 적용합니다.
원칙: 핵심 구문은 P in K
를 사용하여 키를 반복하는 것이며, 여기서 K
는 일반적으로 keyof SomeType
에서 가져온 속성 키의 유니온입니다. 매핑 내에서 속성 이름(키 재매 핑을 위해 as
사용) 및/또는 유형을 수정할 수 있습니다.
구현 및 적용:
일반적인 시나리오는 객체의 모든 속성을 readonly
또는 optional
로 만드는 것입니다. TypeScript는 Partial<T>
및 Readonly<T>
와 같은 내장 매핑된 유형을 제공하지만, 자신의 유형을 만들기 위해 기본 메커니즘을 이해하는 것이 중요합니다.
type Coordinates = { x: number; y: number; z: number; }; // 예시 1: 모든 속성을 null로 만들기 type Nullable<T> = { [P in keyof T]: T[P] | null; }; type NullableCoordinates = Nullable<Coordinates>; /* type NullableCoordinates = { x: number | null; y: number | null; z: number | null; } */ // 예시 2: 모든 속성을 선택적으로 만들고 읽기 전용으로 만들기 type DeepReadonlyAndOptional<T> = { readonly [P in keyof T]? : T[P]; }; type ReadonlyOptionalCoordinates = DeepReadonlyAndOptional<Coordinates>; /* type ReadonlyOptionalCoordinates = { readonly x?: number; readonly y?: number; readonly z?: number; } */
as
를 사용한 키 재매핑:
매핑된 유형은 속성 이름을 바꿀 수도 있습니다. 이는 데이터 구조를 변환하는 데 강력합니다.
type User = { id: string; name: string; email: string; }; // 키를 대문자로 변환 type UppercaseKeys<T> = { [P in keyof T as Uppercase<P & string>]: T[P]; }; type UserUppercaseKeys = UppercaseKeys<User>; /* type UserUppercaseKeys = { ID: string; NAME: string; EMAIL: string; } */ // "id"를 제거하고 "Ref"를 추가하도록 키 변환 type PropRef<T> = { [P in keyof T as P extends `${infer K}Id` ? `${K}Ref` : P]: T[P]; }; type Product = { productId: string; name: string }; type ProductRef = PropRef<Product>; /* type ProductRef = { productRef: string; name: string; } */
키 재매 핑은 유형 수준에서 복잡한 데이터 마이그레이션 및 API 계약 변환 가능성을 열어줍니다.
infer
키워드
infer
키워드는 항상 조건부 유형의 extends
절 내에서 사용됩니다. 목적은 유형 검사를 통해 추론할 수 있는 새 유형 변수를 선언하는 것입니다.
원칙: infer
는 유형 검사 중에 다른 유형 내의 특정 위치에서 TypeScript가 추론하는 유형의 플레이스홀더 역할을 합니다. 일단 추론되면 이 새 유형 변수는 조건부 유형의 TrueType
분기에서 사용할 수 있습니다.
구현 및 적용:
GetReturnType<T>
를 사용하여 infer
를 이미 사용했습니다. 특히 배열 및 Promise 유형에 대한 더 많은 예제를 살펴보겠습니다.
배열 요소 유형 추론:
type GetArrayElementType<T> = T extends (infer U)[] ? U : never; type Numbers = number[]; type ElementOfNumbers = GetArrayElementType<Numbers>; // number type Strings = string[]; type ElementOfStrings = GetArrayElementType<Strings>; // string type NotAnArray = string; type ElementOfNotAnArray = GetArrayElementType<NotAnArray>; // never
여기서 T extends (infer U)[]
는 T
가 배열 유형인지 확인합니다. 그렇다면 infer U
는 해당 배열 내의 요소 유형을 캡처하고 U
가 결과가 됩니다.
Promise 확인 값 추론:
type GetPromiseResolvedType<T> = T extends Promise<infer U> ? U : T; type MyPromise = Promise<string>; type PromiseResult = GetPromiseResolvedType<MyPromise>; // string type AnotherPromise = Promise<number[]>; type AnotherPromiseResult = GetPromiseResolvedType<AnotherPromise>; // number[] type NotAPromise = boolean; type NotAPromiseResult = GetPromiseResolvedType<NotAPromise>; // boolean
이 유틸리티 유형은 비동기 코드를 다룰 때 .then()
핸들러를 올바르게 유형화할 수 있도록 하는 데 매우 유용합니다.
함수 매개변수 추론:
type GetFunctionParameters<T> = T extends (...args: infer P) => any ? P : never; function doSomething(name: string, age: number): string { return `Name: ${name}, Age: ${age}`; } type DoSomethingParams = GetFunctionParameters<typeof doSomething>; // [name: string, age: number] type SomeOtherFunction = (a: boolean) => void; type SomeOtherFunctionParams = GetFunctionParameters<SomeOtherFunction>; // [a: boolean]
이를 통해 함수 유형에 대한 전체 매개변수 튜플을 추출할 수 있으며, 이는 고차 함수 또는 모킹에 유용할 수 있습니다.
개념 결합: 실제 예시
이러한 개념을 결합하여 더 정교한 유형을 만들어 보겠습니다. API 끝점이 함수로 정의된 세트가 있다고 가정해 보겠습니다. Promise인 경우 해당 반환 유형을 추출하고 그렇지 않으면 그대로 유지하려고 합니다.
type APIResponseMapping = { getUser: (id: string) => Promise<{ id: string; name: string }>; getProducts: () => Promise<Array<{ id: string; name: string; price: number }>>; logEvent: (event: string) => void; // Promise가 아님 }; // Promise의 확인된 유형을 가져오거나 유형 자체를 가져오는 유틸리티 type UnpackPromise<T> = T extends Promise<infer U> ? U : T; // API 끝점 반환 유형을 변환하는 매핑된 유형 type ResolvedAPIResponses<T> = { [K in keyof T]: T[K] extends (...args: any[]) => infer R ? UnpackPromise<R> : never; }; type ProcessedResponses = ResolvedAPIResponses<APIResponseMapping>; /* type ProcessedResponses = { getUser: { id: string; name: string; }; getProducts: { id: string; name: string; price: number; }[]; logEvent: void; } */
이 고급 예에서 ResolvedAPIResponses
는 APIResponseMapping
의 키를 반복하는 매핑된 유형입니다. 각 속성 K
에 대해 먼저 조건부 유형을 사용하여 T[K]
가 함수인지 확인합니다. 그렇다면 반환 유형 R
을 infer
합니다. 그런 다음 UnpackPromise
조건부 유형을 R
에 적용하여 최종 확인된 유형을 가져옵니다. T[K]
가 함수가 아니면 never
로 기본값으로 설정됩니다. 이는 매우 구체적이고 유용한 유형 변환을 만들기 위해 이러한 고급 제네릭 기능을 연결하는 방법을 보여줍니다.
정밀함의 힘
조건부 유형, 매핑된 유형 및 infer
키워드는 단순한 학문적 호기심이 아닙니다. 진정으로 유형 안전하고 유연하며 유지 관리 가능한 TypeScript 애플리케이션을 작성하는 데 필수적인 도구입니다. 복잡한 유형 관계를 표현하고, 유형 수준에서 데이터 모양을 변환하고, 특정 유형 정보를 추출할 수 있도록 하여 강력하고 작업하기 좋은 코드를 만들 수 있습니다. 이러한 고급 제네릭 기술을 마스터하면 기본 유형 안전성을 넘어 강력한 유형 기반 개발 영역으로 이동할 수 있으며, 런타임에만 나타나는 오류를 컴파일 타임에 잡을 수 있습니다. 이러한 개념을 수용하고 프로젝트에서 TypeScript의 전체 잠재력을 발휘하십시오.