TypeScript - Mapped Types: A Beginner's Guide

안녕하세요, 미래의 TypeScript 마법사 여러분! 오늘 우리는 Mapped Types의 세계로 흥미로운 여정을 떠납니다. 프로그래밍에 새로운 사람이라고 걱정하지 마세요 - 저는 친절한 안내자가 되어 step by step 함께 할 것입니다. 이 튜토리얼의 끝을 맺을 때까지, 당신은 프로처럼 타입을 매핑할 수 있을 것입니다!

TypeScript - Mapped Types

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에서 선택 사항이었던 ageemail도 포함됩니다.

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

이 예제에서 BasicPhoneInfoSmartphonebrandmodel 프로퍼티만 포함합니다. 나머지는 제외됩니다.

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

이 예제에서 BookPreviewBookpagesisbn을 제외한 모든 프로퍼티를 포함합니다.

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