매핑된 타입
타입스크립트의 또 다른 강력한 고급 기능은 바로 매핑된 타입(Mapped Types) 입니다. 매핑된 타입은 기존 타입의 각 속성을 순회(iterate)하면서 새로운 타입으로 변환하는 방식입니다. 마치 자바스크립트의 map
함수가 배열의 각 요소를 변환하여 새로운 배열을 만들듯이, 매핑된 타입은 객체 타입의 각 속성을 변환하여 새로운 객체 타입을 생성합니다.
이 기능을 통해 우리는 기존 타입을 기반으로 다양한 변형된 타입을 손쉽게 만들 수 있으며, 이는 타입 정의의 중복을 줄이고 코드의 유연성을 크게 높여줍니다.
매핑된 타입의 기본 문법
매핑된 타입의 기본 문법은 다음과 같습니다.
type NewType<T> = {
[P in K]: SomeType;
};
T
: 변환의 대상이 되는 기존 타입 (주로 객체 타입)P
: 기존 타입T
의 속성 이름 하나하나를 나타내는 타입 변수in
:for...in
루프처럼K
에 있는 속성들을 순회하겠다는 의미K
: 순회할 속성 이름들의 유니온 타입 (주로keyof T
를 사용)SomeType
:P
에 해당하는 새로운 속성에 부여할 타입
예시를 통해 살펴보겠습니다. 기존 객체 타입의 모든 속성을 readonly
로 만드는 Readonly<T>
유틸리티 타입을 직접 구현해 봅시다.
interface User {
id: number;
name: string;
email?: string;
}
// ReadonlyType은 User 타입의 모든 속성을 읽기 전용으로 만듭니다.
type ReadonlyType<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyUser = ReadonlyType<User>;
/*
// ReadonlyUser의 실제 타입은 다음과 같습니다.
type ReadonlyUser = {
readonly id: number;
readonly name: string;
readonly email?: string;
};
*/
const user: ReadonlyUser = {
id: 1,
name: "김타입",
email: "kim@example.com",
};
// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
user.name; // 접근은 가능
이 예시에서 [P in keyof T]
는 T
타입의 모든 속성 이름(id
, name
, email
)을 P
에 할당하며 순회합니다. 그리고 각 P
에 readonly
한정자를 붙이고, 원래 T
타입의 P
속성 타입(T[P]
)을 그대로 사용합니다.
매핑 수정자
매핑된 타입을 사용할 때, 기존 속성의 readonly
또는 ?
(선택적) 한정자를 추가하거나 제거할 수 있습니다. 이를 매핑 수정자(Mapping Modifiers) 라고 합니다.
+
또는-
: 한정자를 추가하거나 제거합니다.+readonly
:readonly
한정자를 추가-readonly
:readonly
한정자를 제거+?
:?
(선택적) 한정자를 추가-?
:?
(선택적) 한정자를 제거
이 +
나 -
기호는 생략될 수 있으며, 생략되면 기본적으로 +
와 동일하게 동작합니다. 예를 들어, readonly [P in keyof T]
는 +readonly [P in keyof T]
와 같습니다.
예시를 통해 각각의 사용법을 살펴보겠습니다.
모든 속성을 선택적으로 만들기 (Partial<T>
구현)
type PartialType<T> = {
[P in keyof T]?: T[P]; // '?'를 붙여 모든 속성을 선택적으로 만듭니다.
};
interface Product {
id: number;
name: string;
price: number;
}
type OptionalProduct = PartialType<Product>;
/*
type OptionalProduct = {
id?: number;
name?: string;
price?: number;
};
*/
const p1: OptionalProduct = { name: "키보드" }; // 유효함
const p2: OptionalProduct = {}; // 유효함
모든 속성을 필수로 만들기 (Required<T>
구현)
type RequiredType<T> = {
[P in keyof T]-?: T[P]; // '-'와 '?'를 붙여 모든 속성을 필수로 만듭니다.
};
interface Config {
port?: number;
host?: string;
debug?: boolean;
}
type StrictConfig = RequiredType<Config>;
/*
type StrictConfig = {
port: number;
host: string;
debug: boolean;
};
*/
// const c1: StrictConfig = { port: 8080 }; // Error: 'host' 및 'debug' 속성이 누락됨
const c2: StrictConfig = { port: 3000, host: "localhost", debug: true }; // 유효함
키 다시 매핑
타입스크립트 4.1부터는 매핑된 타입의 키를 변환할 수 있는 키 다시 매핑(Key Remapping) 기능이 추가되었습니다. 이는 as
키워드를 사용하여 새로운 키 이름을 정의함으로써 가능합니다.
type MappedTypeWithNewKeys<T> = {
[P in keyof T as NewKeyType]: T[P];
};
NewKeyType
:P
와 다른 타입 유틸리티를 사용하여 새롭게 생성될 키의 타입.
예시: 객체의 모든 키 앞에 get
을 붙여 Getter 속성처럼 만들기
interface UserProfile {
name: string;
age: number;
isActive: boolean;
}
// Getters라는 새로운 타입을 정의합니다.
// 모든 속성 키 앞에 'get'을 붙이고 첫 글자를 대문자로 변환합니다.
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: T[P];
};
type UserProfileGetters = Getters<UserProfile>;
/*
// UserProfileGetters의 실제 타입은 다음과 같습니다.
type UserProfileGetters = {
getName: string;
getAge: number;
getIsActive: boolean;
};
*/
// 예시 사용:
let userGetters: UserProfileGetters = {
getName: "Alice",
getAge: 30,
getIsActive: true,
};
console.log(userGetters.getName); // Alice
여기서 Capitalize<string & P>
는 P
(원래 속성 이름)의 첫 글자를 대문자로 변환하는 내장 유틸리티 타입입니다. string & P
는 P
가 반드시 string
타입임을 보증합니다.
또 다른 예시: 모든 속성을 onChange
접두사와 함께 콜백 함수 타입으로 변환하기
interface FormFields {
firstName: string;
lastName: string;
email: string;
}
type OnChangeCallbacks<T> = {
[P in keyof T as `onChange${Capitalize<string & P>}`]?: (value: T[P]) => void;
};
type UserFormCallbacks = OnChangeCallbacks<FormFields>;
/*
// UserFormCallbacks의 실제 타입은 다음과 같습니다.
type UserFormCallbacks = {
onChangeFirstName?: (value: string) => void;
onChangeLastName?: (value: string) => void;
onChangeEmail?: (value: string) => void;
};
*/
const formHandler: UserFormCallbacks = {
onChangeFirstName: (val) => console.log(`First name changed to: ${val}`),
onChangeEmail: (val) => console.log(`Email changed to: ${val}`),
};
formHandler.onChangeFirstName?.("John"); // First name changed to: John
formHandler.onChangeEmail?.("john@example.com"); // Email changed to: john@example.com
내장 매핑된 유틸리티 타입
타입스크립트는 매핑된 타입을 기반으로 미리 정의된 여러 유용한 유틸리티 타입들(Built-in Mapped Utility Types)을 제공합니다.
Partial<T>
:T
의 모든 속성을 선택적으로 만듭니다. ([P in keyof T]?: T[P]
)Required<T>
:T
의 모든 선택적 속성을 필수로 만듭니다. ([P in keyof T]-?: T[P]
)Readonly<T>
:T
의 모든 속성을 읽기 전용으로 만듭니다. (readonly [P in keyof T]: T[P]
)Pick<T, K>
:T
에서K
에 해당하는 속성들만 선택하여 새로운 타입을 만듭니다. ([P in K]: T[P]
)Omit<T, K>
:T
에서K
에 해당하는 속성들을 제외한 새로운 타입을 만듭니다. (Pick<T, Exclude<keyof T, K>>
와 유사)Record<K, T>
:K
의 속성 이름을 가지며, 모든 속성 값이T
타입인 객체 타입을 만듭니다.
이 유틸리티 타입들은 타입스크립트 개발에서 매우 자주 사용되므로, 그 사용법과 목적을 잘 이해하는 것이 중요합니다.
매핑된 타입은 기존 타입을 기반으로 새로운 타입을 생성하고 변형하는 데 있어 엄청난 유연성을 제공합니다. 이를 통해 코드 중복을 피하고, 복잡한 타입 변환 로직을 간결하게 표현할 수 있습니다. 매핑 수정자와 키 다시 매핑 기능을 활용하면 거의 모든 객체 타입 변환 시나리오를 커버할 수 있으며, 이는 타입스크립트 기반 프로젝트의 유지보수성과 확장성을 크게 향상시킵니다.