# 맵드 타입(Mapped Type)이란?

맵드 타입이란 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법을 의미합니다. 마치 자바스크립트 map() API 함수를 타입에 적용한 것과 같은 효과를 가집니다.

맵드 타입 예시

# 자바스크립트의 map 함수란?

자바스크립트의 map API는 배열을 다룰 때 유용한 자바스크립트 내장 API입니다. 간단하게 코드를 보겠습니다.

var arr = [{ id: 1, title: '함수'}, { id: 2, title: '변수'}, { id: 3, title: '인자'}];
var result = arr.map(function(item) {
  return item.title;
});
console.log(result); // ['함수', '변수', '인자'];

위 코드는 3개의 객체를 요소로 가진 배열 arr.map() API를 적용한 코드입니다. 배열의 각 요소를 순회하여 객체(id, title)에서 문자열로 변환하였습니다.

# 맵드 타입의 기본 문법

맵드 타입은 위에서 살펴본 자바스크립트의 map 함수를 타입에 적용했다고 보시면 됩니다. 이를 위해서는 아래와 같은 형태의 문법을 사용해야 합니다.

{ [ P in K ] : T }
{ [ P in K ] ? : T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ? : T }

# 맵드 타입 기본 예제

맵드 타입의 가장 간단한 예제를 보겠습니다. 아래와 같이 헐크, 토르, 캡틴을 유니온 타입으로 묶어주는 Heroes라는 타입이 있다고 하겠습니다.

type Heroes = 'Hulk' | 'Thor' | 'Capt';

여기서 이 세 영웅의 이름에 각각 나이까지 붙인 객체를 만들고 싶다고 한다면 아래와 같이 변환할 수 있습니다.

type HeroProfiles = { [K in Heroes]: number };
const heroInfo: HeroProfiles = {
  Hulk: 54,
  Thor: 1000,
  Capt: 33,
}

위 코드에서 [K in Heroes] 부분은 마치 자바스크립트의 for in 문법과 유사하게 동작합니다. 앞에서 정의한 Heroes 타입의 3개의 문자열을 각각 순회하여 number 타입을 값으로 가지는 객체의 키로 정의가 됩니다. 예를 들면 아래와 같이 말이죠.

{ Hulk: number } // 첫번째 순회
{ Thor: number } // 두번째 순회
{ Capt: number } // 세번째 순회

따라서 위의 원리가 적용된 HeroProfiles의 타입은 아래와 같이 정의됩니다.

type HeroProfiles = {
  Hulk: number;
  Thor: number;
  Capt: number;
}

# 맵드 타입 실용 예제 1

앞에서 살펴본 예제는 맵드 타입의 문법과 동작을 이해하기 위해 간단한 코드를 사용했습니다. 실제로 서비스를 개발할 때는 위와 같은 코드보다는 아래와 같은 코드를 더 많이 사용하게 됩니다.

type Subset<T> = {
  [K in keyof T]?: T[K];
}

위 코드는 키와 값이 있는 객체를 정의하는 타입을 받아 그 객체의 부분 집합을 만족하는 타입으로 변환해주는 문법입니다. 예를 들면 만약 아래와 같은 인터페이스가 있다고 할 때

interface Person {
  age: number;
  name: string;
}

Subset 타입을 적용하면 아래와 같은 객체를 모두 정의할 수 있습니다.

const ageOnly: Subset<Person> = { age: 23 };
const nameOnly: Subset<Person> = { name: 'Tony' };
const ironman: Subset<Person> = { age: 23, name: 'Tony' };
const empty: Subset<Person> = {};

# 맵드 타입 실용 예제 2

아래와 같이 사용자 프로필을 조회하는 API 함수가 있다고 했을 때

interface UserProfile {
  username: string;
  email: string;
  profilePhotoUrl: string;
}

function fetchUserProfile(): UserProfile {
  // ...
}

이 프로필의 정보를 수정하는 API는 아마 아래와 같은 형태일 것입니다.

interface UserProfileUpdate {
  username?: string;
  email?: string;
  profilePhotoUrl?: string;
}

function updateUserProfile(params: UserProfileUpdate) {
  // ...
}

이 때 아래와 같이 동일한 타입에 대해서 반복해서 선언하는 것을 피해야 합니다.

interface UserProfile {
  username: string;
  email: string;
  profilePhotoUrl: string;
}

interface UserProfileUpdate {
  username?: string;
  email?: string;
  profilePhotoUrl?: string;
}

위의 인터페이스에서 반복되는 구조를 아래와 같은 방식으로 재활용 할 수 있습니다.

type UserProfileUpdate = {
  username?: UserProfile['username'];
  email?: UserProfile['email'];
  profilePhotoUrl?: UserProfile['profilePhotoUrl'];
}

혹은 좀 더 줄여서 아래와 같이 정의할 수도 있습니다.

type UserProfileUpdate = {
  [p in 'username' | 'email' | 'profilePhotoUrl']?: UserProfile[p]
}

여기서 위 코드에 keyof를 적용하면 아래와 같이 줄일 수 있습니다.

type UserProfileUpdate = {
  [p in keyof UserProfile]?: UserProfile[p]
}