TypeScript - Mapped Types: A Beginner's Guide
안녕하세요, 미래의 TypeScript 마법사 여러분! 오늘 우리는 Mapped Types의 세계로 흥미로운 여정을 떠납니다. 프로그래밍에 새로운 사람이라고 걱정하지 마세요 - 저는 친절한 안내자가 되어 step by step 함께 할 것입니다. 이 튜토리얼의 끝을 맺을 때까지, 당신은 프로처럼 타입을 매핑할 수 있을 것입니다!
What are Mapped Types?
먼저 Mapped Types이 무엇인지 이해해 보겠습니다. 상상해 보세요. 다양한 초콜릿이 들어있는 상자가 있고, 각 초콜릿을 금 포일로 감싸는 새로운 상자를 만들고 싶습니다. TypeScript에서 Mapped Types이 하는 일은 이와 같습니다 - 기존의 타입을 받아서 규칙에 따라 새로운 타입으로 변환합니다.
Built-in Mapped Types
TypeScript는 매우 유용한 몇 가지 사전 정의된 Mapped Types을 제공합니다. 하나씩 살펴보겠습니다:
1. Partial
Partial<T>
타입은 T
의 모든 프로퍼티를 선택 사항으로 만듭니다. 이는 엄격한 레시피를 유연한 레시피로 바꾸는 것과 같습니다.
interface Recipe {
name: string;
ingredients: string[];
cookingTime: number;
}
type FlexibleRecipe = Partial<Recipe>;
// Now we can create a recipe without all properties
const quickSnack: FlexibleRecipe = {
name: "Toast",
// We can skip ingredients and cookingTime
};
이 예제에서 FlexibleRecipe
는 모든 프로퍼티를 지정하지 않고 레시피를 만들 수 있게 합니다. 오브젝트의 일부를 업데이트할 때 완벽합니다.
2. Required
Required<T>
는 Partial<T>
의 반대입니다. 모든 프로퍼티를 필수로 만듭니다. 원래 타입에서 선택 사항이었던 프로퍼티들도 그렇습니다.
interface UserProfile {
name: string;
age?: number;
email?: string;
}
type CompleteUserProfile = Required<UserProfile>;
// Now we must provide all properties
const user: CompleteUserProfile = {
name: "Alice",
age: 30,
email: "[email protected]"
};
이 예제에서 CompleteUserProfile
은 모든 프로퍼티를 제공해야 합니다. 원래 UserProfile
에서 선택 사항이었던 age
와 email
도 포함됩니다.
3. Readonly
Readonly<T>
는 T
의 모든 프로퍼티를 읽기 전용으로 만듭니다. 이는 타입을 유리 상자에 넣는 것과 같습니다 - 볼 수는 있지만 만지지는 못합니다!
interface Toy {
name: string;
price: number;
}
type CollectibleToy = Readonly<Toy>;
const actionFigure: CollectibleToy = {
name: "Superhero",
price: 19.99
};
// This will cause an error
// actionFigure.price = 29.99;
이 예제에서 actionFigure
를 만들면 프로퍼티를 수정할 수 없습니다. 불변 오브젝트를 만드는 데 적합합니다.
4. Pick<T, K>
Pick<T, K>
는 T
에서 지정된 프로퍼티 K
만 선택하여 새로운 타입을 만듭니다. 이는 타입에서 좋아하는 기능을 채집하는 것과 같습니다.
interface Smartphone {
brand: string;
model: string;
year: number;
color: string;
price: number;
}
type BasicPhoneInfo = Pick<Smartphone, 'brand' | 'model'>;
const myPhone: BasicPhoneInfo = {
brand: "TechBrand",
model: "X2000"
};
이 예제에서 BasicPhoneInfo
는 Smartphone
의 brand
와 model
프로퍼티만 포함합니다. 나머지는 제외됩니다.
5. Omit<T, K>
Omit<T, K>
는 Pick<T, K>
의 반대입니다. T
에서 지정된 프로퍼티 K
를 제거하여 새로운 타입을 만듭니다.
interface Book {
title: string;
author: string;
pages: number;
isbn: string;
}
type BookPreview = Omit<Book, 'pages' | 'isbn'>;
const preview: BookPreview = {
title: "TypeScript Adventures",
author: "Code Wizard"
};
이 예제에서 BookPreview
는 Book
의 pages
와 isbn
을 제외한 모든 프로퍼티를 포함합니다.
Examples of Using Mapped Types
이제 사전 정의된 Mapped Types을 보고, 실제 상황에서 어떻게 사용할 수 있는지 살펴보겠습니다.
Example 1: Creating a Form State
폼을 만들고, 어떤 필드가 수정되었는지 추적하고 싶은 상황을 상상해 보세요:
interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}
type FormTouched = { [K in keyof LoginForm]: boolean };
const touchedFields: FormTouched = {
username: true,
password: false,
rememberMe: true
};
이 예제에서 FormTouched
타입은 LoginForm
과 같은 키를 가지지만, 모든 값은 부울 타입으로, 필드가 터치되었는지 나타냅니다.
Example 2: API Response Wrapper
서로 다른 타입의 데이터를 반환하는 API가 있고, 각 응답을 표준 형식으로 감싸고 싶은 상황을 상상해 보세요:
interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
timestamp: number;
}
type UserData = { id: number; name: string; email: string };
type ProductData = { id: number; name: string; price: number };
const userResponse: ApiResponse<UserData> = {
data: { id: 1, name: "John Doe", email: "[email protected]" },
status: 'success',
timestamp: Date.now()
};
const productResponse: ApiResponse<ProductData> = {
data: { id: 101, name: "Laptop", price: 999.99 },
status: 'success',
timestamp: Date.now()
};
이 예제는 제네릭을 사용하여 Mapped Types을 만들어 유연하고 재사용 가능한 타입 구조를 만드는 방법을 보여줍니다.
Creating Custom Mapped Types
이제 TypeScript 실력을 펼쳐보고 커스텀 Mapped Types을 만들어 보겠습니다!
Custom Type 1: Nullable
모든 프로퍼티를 선택 사항으로 만드는 타입을 만들어 보겠습니다:
type Nullable<T> = { [K in keyof T]: T[K] | null };
interface Person {
name: string;
age: number;
}
type NullablePerson = Nullable<Person>;
const maybePerson: NullablePerson = {
name: "Jane",
age: null // 이제 유효합니다
};
우리의 Nullable<T>
타입은 모든 프로퍼티가 원래 타입이나 null
이 될 수 있게 합니다.
Custom Type 2: Freezable
오브젝트에 freeze
메서드를 추가하는 타입을 만들어 보겠습니다:
type Freezable<T> = T & { freeze(): Readonly<T> };
interface Config {
theme: string;
fontSize: number;
}
function makeFreezable<T>(obj: T): Freezable<T> {
return {
...obj,
freeze() {
return Object.freeze({ ...this }) as Readonly<T>;
}
};
}
const config = makeFreezable<Config>({
theme: "dark",
fontSize: 14
});
const frozenConfig = config.freeze();
// frozenConfig.theme = "light"; // 이제 오류를 발생시킵니다
이 커스텀 타입은 모든 오브젝트에 freeze
메서드를 추가하여 불변 오브젝트를 만들 수 있게 합니다.
Conclusion
와우, 오늘 많은 내용을 다루었습니다! 내장된 Mapped Types에서 커스텀 타입을 만드는 것까지, TypeScript의 강력하고 유연한 면모를 보았습니다. Mapped Types은 TypeScript 도구箱에서 마법의 지팡이와 같습니다 - 타입을 변환하고 조작하는 데 매우 유용합니다.
Mapped Types을 마스터하려면 연습이 중요합니다. 자신만의 타입을 만들고, 다양한 조합을 시도하면 곧 기능적이고 우아하며 타입 안전한 TypeScript 코드를 작성할 수 있을 것입니다.
계속 코딩하고, 배우고, TypeScript를 즐기세요!
Credits: Image by storyset