타입스크립트의 강력한 맵드 타입과 조건부 타입을 다루는 포괄적인 가이드입니다. 실용적인 예제와 고급 활용 사례를 통해 견고하고 타입-안전한 애플리케이션을 만드는 방법을 배워보세요.
TypeScript의 맵드 타입과 조건부 타입 마스터하기
자바스크립트의 상위 집합인 타입스크립트는 견고하고 유지보수하기 쉬운 애플리케이션을 만들기 위한 강력한 기능들을 제공합니다. 이러한 기능들 중에서 맵드 타입(Mapped Types)과 조건부 타입(Conditional Types)은 고급 타입 조작을 위한 필수 도구로 돋보입니다. 이 가이드는 이러한 개념에 대한 포괄적인 개요를 제공하며, 그 구문, 실제 적용 사례 및 고급 활용 사례를 탐색합니다. 숙련된 타입스크립트 개발자이든 이제 막 여정을 시작한 분이든, 이 글은 이러한 기능들을 효과적으로 활용할 수 있는 지식을 제공할 것입니다.
맵드 타입이란 무엇인가요?
맵드 타입을 사용하면 기존 타입을 변환하여 새로운 타입을 만들 수 있습니다. 기존 타입의 속성들을 순회하면서 각 속성에 변환을 적용합니다. 이는 모든 속성을 선택적이거나 읽기 전용으로 만드는 등 기존 타입의 변형을 생성하는 데 특히 유용합니다.
기본 구문
맵드 타입의 구문은 다음과 같습니다:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: 매핑하려는 입력 타입입니다.K in keyof T
: 입력 타입T
의 각 키를 순회합니다.keyof T
는T
에 있는 모든 속성 이름의 유니온을 생성하고,K
는 반복 중 각 개별 키를 나타냅니다.Transformation
: 각 속성에 적용하려는 변환입니다. 이는readonly
나?
와 같은 수정자를 추가하거나, 타입을 변경하거나, 또는 전혀 다른 것일 수 있습니다.
실용적인 예제
속성을 읽기 전용으로 만들기
사용자 프로필을 나타내는 인터페이스가 있다고 가정해 봅시다:
interface UserProfile {
name: string;
age: number;
email: string;
}
모든 속성이 읽기 전용인 새로운 타입을 만들 수 있습니다:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
이제 ReadOnlyUserProfile
은 UserProfile
과 동일한 속성을 갖지만, 모두 읽기 전용이 됩니다.
속성을 선택적으로 만들기
비슷하게, 모든 속성을 선택적으로 만들 수 있습니다:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
은 UserProfile
의 모든 속성을 갖지만, 각 속성은 선택 사항이 됩니다.
속성 타입 수정하기
각 속성의 타입을 수정할 수도 있습니다. 예를 들어, 모든 속성을 문자열로 변환할 수 있습니다:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
이 경우, StringifiedUserProfile
의 모든 속성은 string
타입이 됩니다.
조건부 타입이란 무엇인가요?
조건부 타입을 사용하면 조건에 따라 달라지는 타입을 정의할 수 있습니다. 이는 타입이 특정 제약 조건을 만족하는지 여부에 따라 타입 관계를 표현하는 방법을 제공합니다. 이것은 자바스크립트의 삼항 연산자와 유사하지만, 타입을 위한 것입니다.
기본 구문
조건부 타입의 구문은 다음과 같습니다:
T extends U ? X : Y
T
: 확인되는 타입입니다.U
:T
에 의해 확장되는 타입(조건)입니다.X
:T
가U
를 확장할 경우 (조건이 참일 때) 반환될 타입입니다.Y
:T
가U
를 확장하지 않을 경우 (조건이 거짓일 때) 반환될 타입입니다.
실용적인 예제
타입이 문자열인지 확인하기
입력 타입이 문자열이면 string
을, 그렇지 않으면 number
를 반환하는 타입을 만들어 봅시다:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
유니온에서 타입 추출하기
조건부 타입을 사용하여 유니온 타입에서 특정 타입을 추출할 수 있습니다. 예를 들어, null이 아닌 타입을 추출하려면 다음과 같이 합니다:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
여기서 T
가 null
또는 undefined
이면 타입은 never
가 되고, 이는 타입스크립트의 유니온 타입 단순화에 의해 필터링됩니다.
타입 추론하기
조건부 타입은 infer
키워드를 사용하여 타입을 추론하는 데에도 사용될 수 있습니다. 이를 통해 더 복잡한 타입 구조에서 타입을 추출할 수 있습니다.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
이 예제에서 ReturnType
은 함수의 반환 타입을 추출합니다. T
가 임의의 인수를 받고 타입 R
을 반환하는 함수인지 확인합니다. 만약 그렇다면 R
을 반환하고, 그렇지 않다면 any
를 반환합니다.
맵드 타입과 조건부 타입 결합하기
맵드 타입과 조건부 타입의 진정한 힘은 이들을 결합할 때 나타납니다. 이를 통해 매우 유연하고 표현력이 풍부한 타입 변환을 만들 수 있습니다.
예제: Deep Readonly
일반적인 사용 사례는 중첩된 속성을 포함하여 객체의 모든 속성을 읽기 전용으로 만드는 타입을 생성하는 것입니다. 이는 재귀적인 조건부 타입을 사용하여 달성할 수 있습니다.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
여기서 DeepReadonly
는 모든 속성과 그 중첩된 속성에 readonly
수정자를 재귀적으로 적용합니다. 만약 속성이 객체이면 해당 객체에 대해 DeepReadonly
를 재귀적으로 호출합니다. 그렇지 않으면 속성에 단순히 readonly
수정자를 적용합니다.
예제: 타입별로 속성 필터링하기
특정 타입의 속성만 포함하는 타입을 만들고 싶다고 가정해 봅시다. 맵드 타입과 조건부 타입을 결합하여 이를 달성할 수 있습니다.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
이 예제에서 FilterByType
은 T
의 속성들을 순회하며 각 속성의 타입이 U
를 확장하는지 확인합니다. 만약 그렇다면 결과 타입에 해당 속성을 포함시키고, 그렇지 않다면 키를 never
로 매핑하여 제외합니다. 키를 리매핑하기 위해 "as"를 사용하는 점에 유의하세요. 그런 다음 `Omit`과 `keyof StringProperties`를 사용하여 원본 인터페이스에서 문자열 속성을 제거합니다.
고급 활용 사례 및 패턴
기본적인 예제를 넘어서, 맵드 타입과 조건부 타입은 고도로 사용자 정의 가능하고 타입-안전한 애플리케이션을 만들기 위한 더 고급 시나리오에서 사용될 수 있습니다.
분배적 조건부 타입 (Distributive Conditional Types)
조건부 타입은 확인되는 타입이 유니온 타입일 때 분배적(distributive)입니다. 이는 조건이 유니온의 각 멤버에 개별적으로 적용되고, 그 결과가 새로운 유니온 타입으로 결합된다는 것을 의미합니다.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
이 예제에서 ToArray
는 유니온 string | number
의 각 멤버에 개별적으로 적용되어 string[] | number[]
가 됩니다. 만약 조건이 분배적이지 않았다면 결과는 (string | number)[]
가 되었을 것입니다.
유틸리티 타입 사용하기
타입스크립트는 맵드 타입과 조건부 타입을 활용하는 여러 내장 유틸리티 타입을 제공합니다. 이러한 유틸리티 타입은 더 복잡한 타입 변환을 위한 구성 요소로 사용될 수 있습니다.
Partial<T>
:T
의 모든 속성을 선택적으로 만듭니다.Required<T>
:T
의 모든 속성을 필수로 만듭니다.Readonly<T>
:T
의 모든 속성을 읽기 전용으로 만듭니다.Pick<T, K>
:T
에서 속성 집합K
를 선택합니다.Omit<T, K>
:T
에서 속성 집합K
를 제거합니다.Record<K, T>
: 타입이T
인 속성 집합K
를 갖는 타입을 구성합니다.Exclude<T, U>
:T
에서U
에 할당 가능한 모든 타입을 제외합니다.Extract<T, U>
:T
에서U
에 할당 가능한 모든 타입을 추출합니다.NonNullable<T>
:T
에서null
과undefined
를 제외합니다.Parameters<T>
: 함수 타입T
의 매개변수 타입을 얻습니다.ReturnType<T>
: 함수 타입T
의 반환 타입을 얻습니다.InstanceType<T>
: 생성자 함수 타입T
의 인스턴스 타입을 얻습니다.
이러한 유틸리티 타입들은 복잡한 타입 조작을 단순화할 수 있는 강력한 도구입니다. 예를 들어, Pick
과 Partial
을 결합하여 특정 속성만 선택적으로 만드는 타입을 생성할 수 있습니다:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
이 예제에서 OptionalDescriptionProduct
는 Product
의 모든 속성을 가지지만, description
속성은 선택 사항입니다.
템플릿 리터럴 타입 사용하기
템플릿 리터럴 타입을 사용하면 문자열 리터럴을 기반으로 타입을 생성할 수 있습니다. 이는 맵드 타입 및 조건부 타입과 결합하여 동적이고 표현력이 풍부한 타입 변환을 만드는 데 사용될 수 있습니다. 예를 들어, 모든 속성 이름에 특정 문자열 접두사를 붙이는 타입을 만들 수 있습니다:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
이 예제에서 PrefixedSettings
는 data_apiUrl
과 data_timeout
속성을 갖게 됩니다.
모범 사례 및 고려사항
- 단순함 유지: 맵드 타입과 조건부 타입은 강력하지만 코드를 더 복잡하게 만들 수도 있습니다. 타입 변환을 가능한 한 단순하게 유지하려고 노력하세요.
- 유틸리티 타입 사용: 가능할 때마다 타입스크립트의 내장 유틸리티 타입을 활용하세요. 이들은 잘 테스트되었으며 코드를 단순화할 수 있습니다.
- 타입 문서화: 타입 변환, 특히 복잡한 경우 명확하게 문서화하세요. 이는 다른 개발자들이 코드를 이해하는 데 도움이 됩니다.
- 타입 테스트: 타입스크립트의 타입 검사를 사용하여 타입 변환이 예상대로 작동하는지 확인하세요. 단위 테스트를 작성하여 타입의 동작을 검증할 수 있습니다.
- 성능 고려: 복잡한 타입 변환은 타입스크립트 컴파일러의 성능에 영향을 줄 수 있습니다. 타입의 복잡성에 유의하고 불필요한 계산을 피하세요.
결론
맵드 타입과 조건부 타입은 타입스크립트의 강력한 기능으로, 매우 유연하고 표현력이 풍부한 타입 변환을 가능하게 합니다. 이러한 개념을 마스터함으로써 타입스크립트 애플리케이션의 타입 안전성, 유지보수성 및 전반적인 품질을 향상시킬 수 있습니다. 속성을 선택적이거나 읽기 전용으로 만드는 간단한 변환부터 복잡한 재귀 변환 및 조건부 로직에 이르기까지, 이러한 기능들은 견고하고 확장 가능한 애플리케이션을 구축하는 데 필요한 도구를 제공합니다. 이러한 기능들의 잠재력을 최대한 발휘하고 더 능숙한 타입스크립트 개발자가 되기 위해 계속해서 탐색하고 실험해 보세요.
타입스크립트 여정을 계속하면서 공식 타입스크립트 문서, 온라인 커뮤니티, 오픈 소스 프로젝트 등 사용 가능한 풍부한 리소스를 활용하는 것을 잊지 마세요. 맵드 타입과 조건부 타입의 힘을 받아들이면 가장 어려운 타입 관련 문제에도 잘 대처할 수 있을 것입니다.