...
타입스크립트 맵드 타입
타입스크립트의 고급 타입인 맵드 타입(mapped type)이란 기존에 정의되어 있는 타입을 새로운 타입으로 변환해 주는 문법을 의미 한다.
예를 들어 인터페이스에 있는 모든 속성을 루프문 같이 순회해서 optional(?) 로 바꾸거나 readonly 로 지정할수 있으며, 아예 지정된 타입을 바꿔서 변경된 타입을 반환할 수 도 있다.
아직 맵드 타입에 대해 문법을 배우지는 않았지만 간단하게 살펴보자면 다음과 같다.
interface Obj {
prop1: string;
prop2: string;
}
type ChangeType<T> = {
[K in keyof T]: number;
};
type Result = ChangeType<Obj>;
/*
{
prop1: number;
prop2: number;
}
*/
Obj 라는 인터페이스의 객체 속성 타입 string을 ChangeType<Obj> 을 통해 Obj 타입들을 number로 모두 바꿔주고 Result 타입 별칭에게 반환하였다.
그래서 결과적으로 타입 Result는 { prop1: number; prop2; number } 와 같은 객체 타입을 가지게 되었다.
이는 마치 자바스크립트로 따지면, 객체의 속성의 value를 함수에서 for in 으로 객체를 순회해 각 속성의 값들을 문자열에서 숫자로 바꿔주는 것과 비슷해 보인다.
const Obj = {
prop1: "홍길동",
prop2: "홍길동"
}
function ChangeValue(T) {
for(let K in T) { T[K] = 1000; }
return T;
}
const Result = ChangeValue(Obj);
/*
{
prop1: 1000,
prop2: 1000
}
*/
타입스크립트는 타입을 따로 다루는 언어이니, 위의 로직에서 value가 아니라 type으로 바꿔 생각해보면 판박이라고 보면 된다.
맵드 타입 문법
이처럼 맵드 타입은 객체의 속성들을 순회해서 속성의 타입을 다른 타입으로 바꿔주는 역할을 한다.
객체 타입의 속성들을 순회하기 때문에 이를 응용해서, 모든 객체의 속성들을 순회해서 optional(?) 로 바꾸거나 readonly 로 지정할수도 있다.
기존에는 이렇게 인터페이스로 일일히 따로따로 지정하던걸,
interface PersonPartial {
name?: string;
age?: number;
}
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
맵드 타입 문법을 이용해서 마치 함수를 이용하는 것처럼 속성들을 순회해서 변경해주고 그 결과값을 type alias에게 변환해 준다. 보다 시피 맵드 타입은 제네릭과 결합하면 매우매우 강력해진다.
interface Person {
name: string;
age: number;
}
type ReadOnly<T> = {
readonly [P in keyof T]: T[P];
};
type ParTial<T> = {
[P in keyof T]?: T[P];
};
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
이제 맵드 타입 이라는 기능에 대해 익숙해졌으니, 맵드 타입 구성 문법에 대해 차근차근 알아보자.
interface Obj {
name: string;
email: string;
}
type ObjNumber = {
[P in keyof Obj]: number // 맵드 타입
};
/*
type ObjNumber = {
name: number;
email: number;
}
*/
위 코드에서 보다시피, 맵드 타입은 만들어지는 반환 값이 객체 타입 형태이기 때문에 중괄호로 둘러쌓여져 있으며, 중괄호 안의 대괄호는 키 부분을 나타낸다.
마치 인터페이스의 indexable 타입을 생각하면 된다.
그런데 인터페이스의 indexable 타입과 다른점이 있는데 바로 대괄호 안에 in 키워드를 사용한다는 점이다.
in 연산자는 자바스크립트의 for in 으로 생각하면 편하다.
for in 은 객체의 key를 순회하는 루프문인데 이 역할을 똑같이 따라한다고 보면 된다.
그리고 대괄호 안의 key의 명칭은 indexable 타입이나 제네릭 같이 마음대로 지어도된다. for in 으로 따지면 for (let i in obj) 에서의 변수 i 와 같은 개념이다. (in으로 순회하는 값들을 저장하는 임시 변수)
마지막으로 keyof 는 인터페이스 Obj의 객체 타입 속성들을 뽑아 유니온으로 name | email 만들어주는 역할을 한다. (참고 글)
맵드 타입 활용 예제
맵드 타입은 그저 떠올리는 것보단, 실전 예제 코드를 통해 익히는 것이 좋다.
다음 예제 코드들을 보며 머릿속에 익혀보자.
맵드 타입으로 객체 타입 생성
유니온으로 속성명들을 지정해주고 in 키워드로 순회해 주면서 지정한 속명들의 타입을 boolean 이나 number 로 지정하고, 완성된 타입을 타입 별칭에 반환하는 예제 이다.
type T1 = {
[K in "prop1" | "prop2"]: boolean
};
/*
type T1 = {
prop1: boolean;
prop2: boolean;
}
*/
type T2 = {
[K in "prop1" | "prop2"]: number
};
/*
type T2 = {
prop1: number;
prop2: number;
}
*/
type T3 = {
[K in "prop1" | "prop2"]: string
};
/*
type T3 = {
prop1: string;
prop2: string;
}
*/
맵드 타입을 제네릭과 결합해서 사용하면 마치 자바스크립트 함수 처럼 타입을 만들고 반환하는 로직을 구성 할 수 있다.
type Prop = 'prop1' | 'prop2';
type Make<T> = {
[K in Prop]: T
};
type T1 = Make<boolean>;
/*
type T1 = {
prop1: boolean;
prop2: boolean;
}
*/
type T2 = Make<number>;
/*
type T2 = {
prop1: number;
prop2: number;
}
*/
type T3 = Make<string>;
/*
type T3 = {
prop1: string;
prop2: string;
}
*/
이렇게 만들어진 타입으로 객체 변수를 선언하여 이용하면 된다.
const obj1: T1 = {
prop1: true,
prop2: false,
};
const obj2: T2 = {
prop1: 123,
prop2: 66666,
};
const obj3: T3 = {
prop1: 'hello',
prop2: 'world',
};
맵드 타입으로 객체 속성 제거
아래와 같이 사용자 프로필을 조회하여 객체를 반환하는 API 함수가 있다고 가정한다.
interface UserProfile {
username: string;
email: string;
profilePhotoUrl: string;
}
// 만약 api를 요청해 유저 프로파일을 응답하는 http 함수라고 가정한다면
function fetchUserProfile(): UserProfile {
return {
username: '홍길동',
email: 'hongildong@naver.com',
profilePhotoUrl: 'image',
};
}
const user: UserProfileUpdate = fetchUserProfile();
만일 사용자가 웹에서 프로필 정보를 수정한다면, 이 프로필 객체의 정보를 수정하는 API는 UserProfileUpdate 라고 가정하자.
여기서 optional 키워드만 붙인 똑같은 형태의 인터페이스를 생성하는 이유는, 기존의 UserProfile 인터페이스 타입은 강하게 타입이 정해져있기 때문에, 객체의 속성을 삭제하기 위해서는 이런식을 조치를 취해 주어야 한다.
interface UserProfileUpdate {
username?: string;
email?: string;
profilePhotoUrl?: string;
}
// 사용자가 자신의 프로필 이미지를 삭제 처리를 하여, 유저 객체에서 프로파일 이미지 url 속성을 삭제해서 업데이트하는 api
function updateUserProfile(params: UserProfileUpdate) {
delete params.profilePhotoUrl;
}
updateUserProfile(user);
하지만 같은 모양의 인터페이스를 반복해서 선언하는 것을 가독성에 매우 안좋으니 피해야 한다.
interface UserProfile {
username: string;
email: string;
profilePhotoUrl: string;
}
interface UserProfileUpdate {
username?: string;
email?: string;
profilePhotoUrl?: string;
}
그래서 위의 인터페이스에서 반복되는 구조를 아래와 같은 방식으로 재활용 할 수 있다.
interface UserProfile {
username: string;
email: string;
profilePhotoUrl: string;
}
type UserProfileUpdate = {
[p in 'username' | 'email' | 'profilePhotoUrl']?: UserProfile[p];
};
/*
type UserProfileUpdate = {
username?: string | undefined;
email?: string | undefined;
profilePhotoUrl?: string | undefined;
}
*/
여기서 위 코드에 keyof 인터페이스 를 적용하면, 자동으로 객체 속성들을 모아 유니온 타입으로 변환해줘서, 아래와 같이 줄일 수 있다.
interface UserProfile {
username: string;
email: string;
profilePhotoUrl: string;
}
type UserProfileUpdate = {
[p in keyof UserProfile]?: UserProfile[p];
};
인터페이스 타입 바꾸기
interface Person {
name: string;
age: number;
}
type MakeBoolean<T> = {
[P in keyof T]?: boolean
};
const pMap: MakeBoolean<Person> = {}; // Person 인터페이스의 타입들을 모두 boolean으로 변경하고 optional로 지정
/*
{
name?: boolean | undefiened;
age?: boolean | undefiened;
}
*/
pMap.name = true;
pMap.age = false;
pMap.age = undefined; // 선택 속성이기에 undefiened 할당 가능
readonly / optional 붙이기
위에서 한번 소개한 맵드 타입의 대표적인 기법이다.
그런데 여기서 주의 깊게 봐야 할 점은 T[P] 부분인데, 이 키워드의 의미는 제네릭으로 받은 속성의 타입을 유지한다 라는 의미이다.
예를 들어 Partial<Person> 맵드 타입을 호출하면, 제네릭 T에 Person이 들어올꺼고, 맵드 타입 P에 Person의 keyof인 (name | age) 유니온 타입이 순회되어 하나씩 P에 들어오게 된다.
결국 T[P] 는 Person[name] 과 Person[age] 가 될 것이고, 이는 곧 속성 자기 자신의 타입 밸류를 가리키는 것이니, 자신의 속성의 타입을 복사해 그대로 반환한다는 의미이다.
interface Person {
name: string;
age: number;
}
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
type PersonPartial = Partial<Person>;
/*
type PersonPartial = {
name?: string | undefined;
age?: number | undefined;
}
*/
type ReadonlyPerson = Readonly<Person>;
/*
type ReadonlyPerson = {
readonly name: string;
readonly age: number;
}
*/
readonly / optional 떼버리기
속성들을 순회해서 readonly 와 ? 를 붙여봤으면 이번엔 제거해보는 맵드 타입을 만들어 보자.
여기서 새로운 연산자가 나오는데 바로 마이너스 - 연산자이다. 어렵게 이해할 필요없이 제거의 의미를 내포한다.
interface Person1 {
name?: string;
age?: number;
}
interface Person2 {
readonly name: string;
readonly age: number;
}
type Exclude_ReadOnly<T> = {
// -readonly 라는 뜻은 readonly를 떼라 라는 의미이다.
// 반대로 +readonly 는 readonly를 추가하라는 의미인데 정수를 +1로 안쓰듯이 그냥 생략 가능함
-readonly [P in keyof T]: T[P];
};
type Exclude_ParTial<T> = {
// -? 라는 뜻은 ?를 떼라 라는 의미이다.
// 반대로 +? 는 ?를 추가하라는 의미인데 정수를 +1로 안쓰듯이 그냥 생략 가능함
[P in keyof T]-?: T[P];
};
type PersonPartial = Exclude_ParTial<Person1>;
/*
type PersonPartial = {
name: string;
age: number;
}
*/
type ReadonlyPerson = Exclude_ReadOnly<Person2>;
/*
type ReadonlyPerson = {
name: string;
age: number;
}
*/
중첩 객체 속성 만들기
맵드 타입을 이용해 중첩 객체 속성 타입을 이루는 예제이다.
/* eslint-disable */
export {};
interface Person {
name: string;
age: number;
language: string;
}
type Recorded<K extends string, T> = { [P in K]: T };
type T1 = Recorded<'p1' | 'p2', Person>;
const t: T1 = {
p1: {
name: '홍길동',
age: 88,
language: 'kor',
},
p2: {
name: '링컨',
age: 34,
language: 'eng',
},
};
맵드 타입에 새 멤버 추가
멤버를 추가하길 원한다면, 교차 타입을 이용해 구성할 수 있다
interface Person {
name: string;
age: number;
}
type PartialWithNewMember<T> = {
[P in keyof T]?: T[P];
} & { newMember: boolean };
type PersonNew = PartialWithNewMember<Person>;
/*
type PersonNew = {
name?: string | undefined;
age?: number | undefined;
newMember: boolean;
}
*/
const person2: PersonNew = {
name: '홍길동',
newMember: true,
};
Enum 타입 가드 하기
맵드 타입을 이용하면 Enum 타입의 활용도를 높일 수 있다.
enum Fruit {
Apple,
Banana,
Orange,
}
const FRUIT_PRICE: { [key in Fruit]: number } = {
[Fruit.Apple]: 1000,
[Fruit.Banana]: 1500,
[Fruit.Orange]: 2000,
};
예를 들어서 위의 코드와 같이 Fruit 라는 enum 타입이 있는데, FRUIT_PRICE 라는 상수에서 모든 과일 가격을 관리한다고 가정을 해보면, 만약 enum 타입이 변화가 있을때 다음과 같이 빨간줄로 경고를 해준다.
이처럼 코드 수정이 필요하면 맵드 타입에 의해서 빨간줄이 나와 실수를 줄일 수 있다.
유틸리티 타입 소개
맵드 타입의 원리를 이해하는데 있어 이 글을 끝까지 보신 여러분들께 노고를 치하한다.
이처럼 맵드 타입 문법을 이용해 충분히 타입을 변환할 수 있지만, 유틸리티 타입을 쓰면 이미 정의해 놓은 타입을 변환하거나 코드를 줄여 간결하게 정의할 수 있다.
사실 위에서 다루었던, Readonly 나 Partial, Pick 등은 원래 이미 타입스크립트 개발진들이 만들어 놓은 맵드 타입들이다. 이러한 미리 만들어진 맵드 타입을 유틸리티 타입이라고 부른다고 이해하면 된다.
유틸리티 타입에 대해서는 다음 포스팅을 참고하길 바란다.
interface Product {
name: string;
phone: number;
address: string;
company: string;
}
type UpdateProduct = Partial<Product> // 모든 속성을 옵셔널로 만들어주는 유틸리티 타입
/*
type UpdateProduct = {
name?: string | undefined;
phone?: number | undefined;
address?: string | undefined;
company?: string | undefined;
}
*/
function updateProductItem(productItem: UpdateProduct) {
}
# 참고자료
https://typescript-kr.github.io/pages/advanced-types.html
이재승 타입스크립트 시작하기
제로초 타입스크립트 올인원
쉽게 시작하는 타입스크립트 (길벗, 2023, 캡틴판교 지음)
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.