한국어

타입스크립트의 강력한 맵드 타입과 조건부 타입을 다루는 포괄적인 가이드입니다. 실용적인 예제와 고급 활용 사례를 통해 견고하고 타입-안전한 애플리케이션을 만드는 방법을 배워보세요.

TypeScript의 맵드 타입과 조건부 타입 마스터하기

자바스크립트의 상위 집합인 타입스크립트는 견고하고 유지보수하기 쉬운 애플리케이션을 만들기 위한 강력한 기능들을 제공합니다. 이러한 기능들 중에서 맵드 타입(Mapped Types)조건부 타입(Conditional Types)은 고급 타입 조작을 위한 필수 도구로 돋보입니다. 이 가이드는 이러한 개념에 대한 포괄적인 개요를 제공하며, 그 구문, 실제 적용 사례 및 고급 활용 사례를 탐색합니다. 숙련된 타입스크립트 개발자이든 이제 막 여정을 시작한 분이든, 이 글은 이러한 기능들을 효과적으로 활용할 수 있는 지식을 제공할 것입니다.

맵드 타입이란 무엇인가요?

맵드 타입을 사용하면 기존 타입을 변환하여 새로운 타입을 만들 수 있습니다. 기존 타입의 속성들을 순회하면서 각 속성에 변환을 적용합니다. 이는 모든 속성을 선택적이거나 읽기 전용으로 만드는 등 기존 타입의 변형을 생성하는 데 특히 유용합니다.

기본 구문

맵드 타입의 구문은 다음과 같습니다:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

실용적인 예제

속성을 읽기 전용으로 만들기

사용자 프로필을 나타내는 인터페이스가 있다고 가정해 봅시다:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

모든 속성이 읽기 전용인 새로운 타입을 만들 수 있습니다:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

이제 ReadOnlyUserProfileUserProfile과 동일한 속성을 갖지만, 모두 읽기 전용이 됩니다.

속성을 선택적으로 만들기

비슷하게, 모든 속성을 선택적으로 만들 수 있습니다:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfileUserProfile의 모든 속성을 갖지만, 각 속성은 선택 사항이 됩니다.

속성 타입 수정하기

각 속성의 타입을 수정할 수도 있습니다. 예를 들어, 모든 속성을 문자열로 변환할 수 있습니다:

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

이 경우, StringifiedUserProfile의 모든 속성은 string 타입이 됩니다.

조건부 타입이란 무엇인가요?

조건부 타입을 사용하면 조건에 따라 달라지는 타입을 정의할 수 있습니다. 이는 타입이 특정 제약 조건을 만족하는지 여부에 따라 타입 관계를 표현하는 방법을 제공합니다. 이것은 자바스크립트의 삼항 연산자와 유사하지만, 타입을 위한 것입니다.

기본 구문

조건부 타입의 구문은 다음과 같습니다:

T extends U ? X : Y

실용적인 예제

타입이 문자열인지 확인하기

입력 타입이 문자열이면 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

여기서 Tnull 또는 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>;

이 예제에서 FilterByTypeT의 속성들을 순회하며 각 속성의 타입이 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)[]가 되었을 것입니다.

유틸리티 타입 사용하기

타입스크립트는 맵드 타입과 조건부 타입을 활용하는 여러 내장 유틸리티 타입을 제공합니다. 이러한 유틸리티 타입은 더 복잡한 타입 변환을 위한 구성 요소로 사용될 수 있습니다.

이러한 유틸리티 타입들은 복잡한 타입 조작을 단순화할 수 있는 강력한 도구입니다. 예를 들어, PickPartial을 결합하여 특정 속성만 선택적으로 만드는 타입을 생성할 수 있습니다:

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">;

이 예제에서 OptionalDescriptionProductProduct의 모든 속성을 가지지만, 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_">;

이 예제에서 PrefixedSettingsdata_apiUrldata_timeout 속성을 갖게 됩니다.

모범 사례 및 고려사항

결론

맵드 타입조건부 타입은 타입스크립트의 강력한 기능으로, 매우 유연하고 표현력이 풍부한 타입 변환을 가능하게 합니다. 이러한 개념을 마스터함으로써 타입스크립트 애플리케이션의 타입 안전성, 유지보수성 및 전반적인 품질을 향상시킬 수 있습니다. 속성을 선택적이거나 읽기 전용으로 만드는 간단한 변환부터 복잡한 재귀 변환 및 조건부 로직에 이르기까지, 이러한 기능들은 견고하고 확장 가능한 애플리케이션을 구축하는 데 필요한 도구를 제공합니다. 이러한 기능들의 잠재력을 최대한 발휘하고 더 능숙한 타입스크립트 개발자가 되기 위해 계속해서 탐색하고 실험해 보세요.

타입스크립트 여정을 계속하면서 공식 타입스크립트 문서, 온라인 커뮤니티, 오픈 소스 프로젝트 등 사용 가능한 풍부한 리소스를 활용하는 것을 잊지 마세요. 맵드 타입과 조건부 타입의 힘을 받아들이면 가장 어려운 타입 관련 문제에도 잘 대처할 수 있을 것입니다.