목차
인덱스드 액세스 타입과 매핑된 타입 이해하기
지난 시간에는 객체 타입에서 키를 꺼내는 방법을 배웠습니다.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}`keyof`를 사용하면 `User`가 가지고 있는 모든 키를 추출할 수 있었습니다.
type UserKey = keyof User;결과는 다음과 같습니다.
"id" | "name" | "email" | "isActive"이번에는 여기서 한 단계 더 들어가겠습니다.
키만 꺼내는 것이 아니라 특정 키에 해당하는 값의 타입을 꺼내고 싶다면 어떻게 해야 할까요?
예를 들어 `name`의 타입만 가져오고 싶습니다.
type UserName = User["name"];결과:
string`id`의 타입을 가져오면?
type UserId = User["id"];결과:
number여러 속성의 타입을 한꺼번에 가져올 수도 있습니다.
type UserInfo =
User["id" | "name"];결과:
number | string그리고 더 재미있는 일이 가능합니다.
`User`의 모든 속성을 자동으로 선택적으로 바꿀 수도 있습니다.
type OptionalUser = {
[Key in keyof User]?:
User[Key];
};결과:
type OptionalUser = {
id?: number;
name?: string;
email?: string;
isActive?: boolean;
};갑자기 문법이 암호문처럼 보이기 시작합니다.
[Key in keyof User]?: User[Key]TypeScript가 갑자기 고대 유적의 벽화를 보여주는 것 같습니다. 🗿
하지만 하나씩 분해하면 생각보다 단순합니다.
keyof User
→ User의 키를 꺼낸다.
Key in keyof User
→ 그 키들을 하나씩 순회한다.
User[Key]
→ 현재 키에 해당하는 타입을 꺼낸다.
?
→ 새 속성을 선택적으로 만든다.즉, TypeScript가 객체 설계도를 들고 방마다 돌아다니면서 새로운 설계도를 만드는 것입니다.
이번 시간에는 타입을 꺼내고, 반복하고, 변환하고, 다시 조립하는 방법을 알아보겠습니다.
1. 이번 시간에 배울 내용
이번 시간에는 다음 내용을 살펴봅니다.
- 인덱스드 액세스 타입이란 무엇인가?
- `User["name"]`은 무엇을 의미하는가?
- 여러 속성의 값 타입을 한꺼번에 가져오는 방법
- `keyof`와 인덱스드 액세스 타입 조합하기
- `T[keyof T]`는 무엇을 의미하는가?
- 배열 요소 타입을 추출하는 방법
- 튜플에서 특정 요소 타입 가져오기
- `typeof`와 함께 사용하는 방법
- 매핑된 타입이란 무엇인가?
- `[Key in keyof T]` 문법 이해하기
- 모든 속성을 선택적으로 변환하기
- 모든 속성을 읽기 전용으로 변환하기
- `?`를 제거해 필수 속성으로 만드는 방법
- `readonly`를 제거하는 방법
- 타입의 값을 다른 타입으로 변환하기
- `Partial`, `Required`, `Readonly`의 내부 원리
- `Pick`과 `Record`가 연결되는 방식
- 키를 재매핑하는 `as`
- 템플릿 리터럴 타입과 매핑된 타입
- 실무에서 타입 변환을 설계하는 방법
이번 시간의 핵심 문장은 다음과 같습니다.
인덱스드 액세스 타입은 설계도에서 부품을 꺼내고, 매핑된 타입은 설계도를 순회하며 새로운 설계도를 만든다.
오늘 TypeScript는 타입 검사기를 잠시 내려놓고 타입 공방의 목수가 됩니다. 🪚
2. 인덱스드 액세스 타입이란?
다음 사용자 타입이 있습니다.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}`name` 속성의 타입만 필요하다고 생각해 보겠습니다.
직접 작성할 수도 있습니다.
type UserName = string;하지만 `User.name`의 타입이 나중에 바뀐다면 문제가 됩니다.
interface User {
id: number;
name: {
firstName: string;
lastName: string;
};
email: string;
isActive: boolean;
}기존 타입은 여전히 `string`입니다.
type UserName = string;두 타입이 어긋났습니다.
대신 원본 타입에서 직접 꺼낼 수 있습니다.
type UserName =
User["name"];이것을 인덱스드 액세스 타입(Indexed Access Type)이라고 합니다.
객체의 값을 가져오는 문법과 닮았습니다.
실제 객체에서는 다음과 같이 값을 꺼냅니다.
user["name"];타입에서는 다음과 같이 타입을 꺼냅니다.
User["name"];비슷하게 생겼지만 하나는 값이고 하나는 타입입니다.
3. 값 접근과 타입 접근 비교하기
실제 객체를 준비하겠습니다.
const user = {
id: 1,
name: "김타입",
email: "type@example.com",
};값을 가져옵니다.
const userName =
user["name"];결과는 실제 값입니다.
김타입이번에는 타입을 준비합니다.
interface User {
id: number;
name: string;
email: string;
}타입을 가져옵니다.
type UserName =
User["name"];결과는 값이 아니라 타입입니다.
string정리하면 다음과 같습니다.
user["name"]
→ 실제 객체의 값
→ "김타입"
User["name"]
→ 객체 타입의 속성 타입
→ string대괄호는 같지만 근무지가 다릅니다.
하나는 JavaScript 창고에서 물건을 꺼내고, 다른 하나는 TypeScript 설계실에서 부품 규격서를 꺼냅니다. 📦
4. 기본적인 인덱스드 액세스 타입
다음 타입을 사용해 하나씩 꺼내 보겠습니다.
interface Product {
id: number;
name: string;
price: number;
isSoldOut: boolean;
}상품 ID 타입:
type ProductId =
Product["id"];결과:
number상품 이름 타입:
type ProductName =
Product["name"];결과:
string가격 타입:
type ProductPrice =
Product["price"];결과:
number판매 상태 타입:
type ProductSoldOut =
Product["isSoldOut"];결과:
boolean원본 타입의 속성 타입이 바뀌면 추출한 타입도 함께 바뀝니다.
5. 존재하지 않는 키는 사용할 수 없다
다음 코드는 가능합니다.
type ProductPrice =
Product["price"];하지만 존재하지 않는 속성은 사용할 수 없습니다.
type ProductCategory =
Product["category"];`Product`에는 `category`가 없기 때문에 TypeScript가 오류를 표시합니다.
이전 시간의 `keyof`와 같은 철학입니다.
설계도에 없는 방
→ 열쇠를 만들 수 없음
설계도에 없는 속성
→ 타입을 꺼낼 수 없음오타도 발견할 수 있습니다.
type ProductName =
Product["naem"];`name`을 `naem`으로 잘못 작성했습니다.
TypeScript가 바로 잡아줍니다.
6. 여러 속성 타입을 한 번에 꺼내기
대괄호 안에는 하나의 키만 넣어야 하는 것은 아닙니다.
유니언 타입을 사용할 수 있습니다.
type UserBasicInfo =
User["id" | "name"];`id`는 `number`입니다.
`name`은 `string`입니다.
따라서 결과는 다음과 같습니다.
number | string세 속성을 가져오겠습니다.
type UserInfo =
User[
"id"
| "name"
| "isActive"
];결과:
number
| string
| boolean여러 방의 물건을 한 상자에 담으면 가능한 물건 종류가 유니언으로 합쳐집니다.
7. 키 타입을 별도로 만들어 사용할 수 있다
먼저 원하는 키를 타입으로 정의할 수 있습니다.
type UserBasicKey =
| "id"
| "name";그 타입을 인덱스로 사용합니다.
type UserBasicValue =
User[UserBasicKey];결과:
number | string키가 복잡할수록 이렇게 분리하면 읽기 쉬울 수 있습니다.
type ContactKey =
| "name"
| "email";
type ContactValue =
User[ContactKey];결과:
string두 속성 모두 문자열이므로 최종 결과도 `string`입니다.
8. `keyof`와 함께 사용하기
지난 시간 배운 `keyof`를 사용하면 모든 키를 가져올 수 있습니다.
type UserKey =
keyof User;결과:
"id"
| "name"
| "email"
| "isActive"이 키를 인덱스드 액세스 타입에 넣어 보겠습니다.
type UserValue =
User[keyof User];각 속성의 타입은 다음과 같습니다.
id
→ number
name
→ string
email
→ string
isActive
→ boolean따라서 결과는 다음과 같습니다.
type UserValue =
number
| string
| boolean;`User[keyof User]`는 다음처럼 읽으면 됩니다.
`User`의 모든 키에 해당하는 값 타입을 가져와라.
9. `T[keyof T]` 패턴 이해하기
이 패턴은 TypeScript에서 꽤 자주 등장합니다.
T[keyof T]단계별로 읽겠습니다.
먼저:
keyof T`T`의 모든 키를 가져옵니다.
그다음:
T[keyof T]그 모든 키의 값 타입을 가져옵니다.
예를 들어 다음 타입이 있습니다.
type Settings = {
theme: string;
fontSize: number;
debug: boolean;
};키:
keyof Settings결과:
"theme"
| "fontSize"
| "debug"값 타입:
Settings[
keyof Settings
]결과:
string
| number
| boolean타입 창고의 모든 서랍을 열고 안에 들어 있는 물건 종류를 한 장의 목록으로 만드는 셈입니다.
10. 선택적 속성의 타입을 꺼내면?
다음 타입에서 `nickname`은 선택적입니다.
interface UserProfile {
name: string;
nickname?: string;
}`nickname` 타입을 가져옵니다.
type Nickname =
UserProfile["nickname"];결과는 다음과 같습니다.
string | undefined선택적 속성이므로 값이 없을 가능성이 있기 때문입니다.
nickname 존재
→ string
nickname 없음
→ undefined따라서 원본 객체 타입의 선택성도 인덱스드 액세스 결과에 반영됩니다.
11. 중첩 객체 타입에서 값 꺼내기
객체 안에 객체가 있을 수 있습니다.
interface User {
id: number;
profile: {
nickname: string;
age: number;
address: {
city: string;
district: string;
};
};
}프로필 타입을 가져옵니다.
type UserProfile =
User["profile"];결과:
{
nickname: string;
age: number;
address: {
city: string;
district: string;
};
}닉네임 타입까지 들어갈 수 있습니다.
type Nickname =
User["profile"]["nickname"];결과:
string주소 타입:
type Address =
User["profile"]["address"];도시 타입:
type City =
User["profile"]
["address"]
["city"];결과:
string객체 설계도의 지하 3층까지 내려가도 TypeScript가 손전등을 들고 따라옵니다. 🔦
12. 중첩 타입을 직접 반복하지 않아도 된다
다음처럼 주소 타입을 직접 다시 작성할 수도 있습니다.
type Address = {
city: string;
district: string;
};하지만 이미 `User`에 같은 구조가 있다면 반복입니다.
type Address =
User["profile"]["address"];이렇게 하면 원본 구조가 바뀌어도 자동으로 따라갑니다.
예를 들어 우편번호를 추가합니다.
interface User {
profile: {
address: {
city: string;
district: string;
zipCode: string;
};
};
}`Address`도 자동으로 변경됩니다.
type Address =
User["profile"]["address"];별도의 수정이 필요하지 않습니다.
13. 배열 요소 타입 꺼내기
배열 타입에서도 인덱스드 액세스 타입을 사용할 수 있습니다.
type Users = User[];배열 요소 타입을 꺼냅니다.
type UserItem =
Users[number];결과:
User왜 `[number]`일까요?
배열은 숫자 인덱스로 요소에 접근하기 때문입니다.
users[0];
users[1];
users[2];따라서 타입에서도 다음처럼 생각할 수 있습니다.
Users[number]
숫자 인덱스로 접근했을 때
나오는 요소의 타입14. 실제 배열에서 요소 타입 추출하기
`typeof`와 함께 사용하면 더욱 유용합니다.
const users = [
{
id: 1,
name: "김타입",
},
{
id: 2,
name: "이컴파일",
},
];배열 타입을 가져옵니다.
type Users =
typeof users;요소 하나의 타입을 가져옵니다.
type User =
typeof users[number];결과는 다음과 비슷합니다.
type User = {
id: number;
name: string;
};실제 데이터 배열에서 객체 타입을 자동으로 추출할 수 있습니다.
15. 배열 값으로 리터럴 유니언 만들기
지난 시간에도 잠깐 살펴봤지만 인덱스드 액세스 타입의 대표 사례입니다.
const themes = [
"light",
"dark",
"system",
] as const;요소 타입을 가져옵니다.
type Theme =
typeof themes[number];결과:
"light"
| "dark"
| "system"과정은 다음과 같습니다.
typeof themes
↓
readonly [
"light",
"dark",
"system"
]
[number]
↓
모든 숫자 인덱스의 요소 타입
결과
↓
"light" | "dark" | "system"16. 튜플에서 특정 위치 타입 가져오기
튜플은 위치별 타입이 정해져 있습니다.
type UserTuple = [
id: number,
name: string,
isActive: boolean,
];첫 번째 요소 타입:
type UserId =
UserTuple[0];결과:
number두 번째 요소 타입:
type UserName =
UserTuple[1];결과:
string세 번째 요소 타입:
type UserStatus =
UserTuple[2];결과:
boolean모든 요소 타입을 가져오려면:
type UserTupleValue =
UserTuple[number];결과:
number
| string
| boolean17. 실제 객체에서 값 타입 추출하기
상태 코드 객체를 만들겠습니다.
const statusCodes = {
success: 200,
badRequest: 400,
notFound: 404,
serverError: 500,
} as const;키 타입:
type StatusName =
keyof typeof statusCodes;결과:
"success"
| "badRequest"
| "notFound"
| "serverError"값 타입:
type StatusCode =
typeof statusCodes[
keyof typeof statusCodes
];결과:
200
| 400
| 404
| 500조금 길어 보입니다.
하지만 읽는 순서는 단순합니다.
typeof statusCodes
→ 객체 타입
keyof typeof statusCodes
→ 객체의 모든 키
typeof statusCodes[
keyof typeof statusCodes
]
→ 모든 값 타입18. 값 타입 추출 패턴을 별칭으로 만들기
반복되는 패턴에 타입 별칭을 만들 수 있습니다.
type ValueOf<T> =
T[keyof T];이제 객체의 모든 값 타입을 쉽게 가져올 수 있습니다.
const colors = {
primary: "#0066ff",
danger: "#ff3333",
success: "#00aa66",
} as const;type Color =
ValueOf<
typeof colors
>;결과:
"#0066ff"
| "#ff3333"
| "#00aa66"직접 작성했던 긴 표현이:
typeof colors[
keyof typeof colors
]다음처럼 짧아졌습니다.
ValueOf<typeof colors>타입 세계에서도 자주 사용하는 조합은 도구함에 넣어둘 수 있습니다.
19. 이제 매핑된 타입을 만나보자
지금까지는 기존 타입에서 원하는 값을 꺼냈습니다.
이번에는 기존 객체 타입을 순회하면서 새로운 타입을 만들어 보겠습니다.
원본 타입:
interface User {
id: number;
name: string;
email: string;
}모든 속성을 선택적으로 만들고 싶습니다.
원하는 결과:
type OptionalUser = {
id?: number;
name?: string;
email?: string;
};직접 다시 작성할 수도 있습니다.
type OptionalUser = {
id?: number;
name?: string;
email?: string;
};하지만 속성이 30개라면요?
그리고 `User`가 변경될 때마다 `OptionalUser`도 수정해야 합니다.
TypeScript에게 시키겠습니다.
type OptionalUser = {
[Key in keyof User]?:
User[Key];
};이것이 매핑된 타입(Mapped Type)입니다.
20. 매핑된 타입이란?
매핑된 타입은 기존 타입의 키를 하나씩 순회하면서 새로운 객체 타입을 만드는 문법입니다.
기본 구조는 다음과 같습니다.
type NewType<T> = {
[Key in keyof T]:
T[Key];
};처음에는 굉장히 복잡해 보입니다.
하지만 하나씩 분리하겠습니다.
keyof T`T`의 모든 키를 가져옵니다.
Key in keyof T그 키들을 하나씩 순회합니다.
T[Key]현재 키의 값 타입을 가져옵니다.
따라서 다음 타입은 원본 타입과 같은 구조를 만듭니다.
type Copy<T> = {
[Key in keyof T]:
T[Key];
};말 그대로 타입 복사기입니다. 📠
21. 매핑된 타입을 반복문처럼 이해하기
다음 타입이 있습니다.
interface User {
id: number;
name: string;
isActive: boolean;
}매핑된 타입을 적용합니다.
type CopyUser = {
[Key in keyof User]:
User[Key];
};TypeScript가 개념적으로 다음 작업을 수행한다고 생각할 수 있습니다.
첫 번째 키
Key = "id"
User["id"]
→ number
결과
id: number다음:
Key = "name"
User["name"]
→ string
결과
name: string다음:
Key = "isActive"
User["isActive"]
→ boolean
결과
isActive: boolean최종 결과:
type CopyUser = {
id: number;
name: string;
isActive: boolean;
};JavaScript 반복문은 아닙니다.
컴파일 단계에서 타입의 키를 순회하는 개념입니다.
22. `in`은 여기서 무슨 의미일까?
매핑된 타입에서 `in`은 JavaScript의 `for...in`과 비슷한 느낌을 줍니다.
[Key in keyof User]다음처럼 읽으면 됩니다.
`User`의 모든 키 중에서 `Key`를 하나씩 꺼낸다.
예를 들어:
keyof User가 다음이라면:
"id"
| "name"
| "email"매핑 과정은 개념적으로:
Key = "id"
Key = "name"
Key = "email"입니다.
`Key`라는 이름은 자유롭게 바꿀 수 있습니다.
type Copy<T> = {
[K in keyof T]:
T[K];
};type Copy<T> = {
[Property in keyof T]:
T[Property];
};둘 다 같은 의미입니다.
23. 모든 속성을 선택적으로 만들기
이제 `?`를 추가해 보겠습니다.
type MyPartial<T> = {
[Key in keyof T]?:
T[Key];
};사용합니다.
interface User {
id: number;
name: string;
email: string;
}type OptionalUser =
MyPartial<User>;결과:
type OptionalUser = {
id?: number;
name?: string;
email?: string;
};이제 일부 속성만 가진 객체도 만들 수 있습니다.
const user:
OptionalUser = {
name: "김타입",
};빈 객체도 가능합니다.
const emptyUser:
OptionalUser = {};모든 방에 “선택 사항” 스티커를 붙인 셈입니다.
24. 이것이 `Partial<T>`의 핵심 원리다
TypeScript에는 이미 `Partial`이라는 유틸리티 타입이 있습니다.
type OptionalUser =
Partial<User>;결과:
{
id?: number;
name?: string;
email?: string;
}직접 만들었던 타입과 매우 비슷합니다.
type MyPartial<T> = {
[Key in keyof T]?:
T[Key];
};즉, `Partial<T>`는 마법 주문이 아닙니다.
매핑된 타입을 이용한 타입 변환 도구입니다.
원본 User
↓
모든 키 순회
↓
각 키에 ? 추가
↓
Partial<User>TypeScript의 내장 유틸리티 타입들이 갑자기 정체를 드러내기 시작합니다. 🎭
25. `Partial`이 유용한 수정 함수
사용자 전체 정보를 받는 타입이 있습니다.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}사용자를 생성할 때는 모든 정보가 필요할 수 있습니다.
function createUser(
user: User
): void {
// 저장
}하지만 사용자를 수정할 때는 일부 속성만 변경하고 싶습니다.
updateUser({
name: "새 이름",
});`User`를 그대로 사용하면 모든 속성을 요구합니다.
function updateUser(
changes: User
): void {}`Partial<User>`를 사용합니다.
function updateUser(
changes:
Partial<User>
): void {
console.log(changes);
}이제 필요한 속성만 전달할 수 있습니다.
updateUser({
name: "이컴파일",
});updateUser({
email:
"new@example.com",
isActive: false,
});26. 모든 속성을 읽기 전용으로 만들기
이번에는 모든 속성에 `readonly`를 붙여보겠습니다.
type MyReadonly<T> = {
readonly [
Key in keyof T
]: T[Key];
};사용합니다.
type ReadonlyUser =
MyReadonly<User>;결과:
type ReadonlyUser = {
readonly id: number;
readonly name: string;
readonly email: string;
readonly isActive: boolean;
};객체를 생성할 수 있습니다.
const user:
ReadonlyUser = {
id: 1,
name: "김타입",
email:
"type@example.com",
isActive: true,
};하지만 변경할 수 없습니다.
user.name =
"이컴파일";오류가 발생합니다.
27. 이것이 `Readonly<T>`의 원리다
TypeScript의 내장 타입을 사용할 수 있습니다.
type ReadonlyUser =
Readonly<User>;직접 만든 구조와 유사합니다.
type MyReadonly<T> = {
readonly [
Key in keyof T
]: T[Key];
};`Readonly<T>`가 하는 일을 말로 풀면 다음과 같습니다.
T의 모든 키를 찾는다.
↓
하나씩 순회한다.
↓
원래 값 타입을 유지한다.
↓
readonly를 붙인다.타입 공방의 직원이 모든 서랍에 자물쇠를 달고 있습니다. 🔒
28. 선택적 속성을 다시 필수로 만들기
매핑된 타입에서는 `?`를 추가할 뿐 아니라 제거할 수도 있습니다.
다음 타입을 살펴보겠습니다.
interface UserProfile {
name?: string;
email?: string;
age?: number;
}모든 속성을 필수로 만들고 싶습니다.
`-?`를 사용합니다.
type MyRequired<T> = {
[Key in keyof T]-?:
T[Key];
};적용합니다.
type RequiredProfile =
MyRequired<
UserProfile
>;결과:
type RequiredProfile = {
name: string;
email: string;
age: number;
};`-?`는 다음 의미입니다.
?
→ 선택적 속성
-?
→ 선택 속성 표시 제거
→ 필수 속성으로 변경29. 이것이 `Required<T>`의 원리다
TypeScript에는 내장 `Required` 타입이 있습니다.
type RequiredProfile =
Required<
UserProfile
>;모든 선택적 속성을 필수로 변환합니다.
직접 구현하면 다음과 비슷합니다.
type MyRequired<T> = {
[Key in keyof T]-?:
T[Key];
};사용 예를 살펴보겠습니다.
기본 설정은 일부 값을 생략할 수 있습니다.
interface AppConfig {
apiUrl?: string;
timeout?: number;
debug?: boolean;
}설정 로딩이 끝난 이후에는 모든 값이 채워졌다고 보장하고 싶을 수 있습니다.
type CompleteConfig =
Required<AppConfig>;const config:
CompleteConfig = {
apiUrl:
"https://api.example.com",
timeout: 3000,
debug: false,
};30. `readonly`도 제거할 수 있다
`readonly`를 제거할 때는 `-readonly`를 사용합니다.
interface ReadonlyUser {
readonly id: number;
readonly name: string;
readonly email: string;
}수정 가능한 타입을 만들어 보겠습니다.
type Mutable<T> = {
-readonly [
Key in keyof T
]: T[Key];
};적용:
type MutableUser =
Mutable<
ReadonlyUser
>;결과:
type MutableUser = {
id: number;
name: string;
email: string;
};이제 속성을 변경할 수 있습니다.
const user:
MutableUser = {
id: 1,
name: "김타입",
email:
"type@example.com",
};
user.name =
"이컴파일";자물쇠 제거 작업 완료입니다. 🔓
31. 매핑 수정자 정리
매핑된 타입에서는 대표적으로 다음 수정자를 사용할 수 있습니다.
?
선택적 속성 추가
-?
선택적 속성 제거
readonly
읽기 전용 추가
-readonly
읽기 전용 제거예를 들어:
type Optional<T> = {
[K in keyof T]?:
T[K];
};type RequiredType<T> = {
[K in keyof T]-?:
T[K];
};type ReadonlyType<T> = {
readonly [
K in keyof T
]: T[K];
};type Mutable<T> = {
-readonly [
K in keyof T
]: T[K];
};TypeScript가 타입 설계도의 옵션 체크박스를 일괄 변경하고 있습니다.
32. 모든 값 타입을 바꾸기
매핑된 타입에서는 원래 값 타입을 그대로 유지할 필요도 없습니다.
다음 사용자 타입이 있습니다.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}모든 속성을 불리언으로 바꿔 보겠습니다.
type BooleanFlags<T> = {
[Key in keyof T]:
boolean;
};적용:
type UserFlags =
BooleanFlags<User>;결과:
type UserFlags = {
id: boolean;
name: boolean;
email: boolean;
isActive: boolean;
};실제로는 각 필드의 표시 여부를 관리할 때 사용할 수 있습니다.
const visibleFields:
UserFlags = {
id: false,
name: true,
email: true,
isActive: false,
};원본 객체의 키는 유지하면서 값의 의미만 바꿨습니다.
33. 필드별 오류 상태 만들기
폼 타입을 준비합니다.
interface SignupForm {
email: string;
password: string;
userName: string;
age: number;
}각 필드에 오류 메시지를 연결하고 싶습니다.
type FormErrors<T> = {
[Key in keyof T]?:
string;
};적용:
type SignupErrors =
FormErrors<
SignupForm
>;결과:
type SignupErrors = {
email?: string;
password?: string;
userName?: string;
age?: string;
};실제 오류 객체:
const errors:
SignupErrors = {
email:
"올바른 이메일을 입력해 주세요.",
password:
"비밀번호는 8자 이상이어야 합니다.",
};폼의 필드가 추가되면 오류 타입에도 자동으로 반영됩니다.
34. 필드별 로딩 상태 만들기
같은 아이디어를 다른 용도로 사용할 수 있습니다.
type LoadingState<T> = {
[Key in keyof T]:
boolean;
};사용자 타입에 적용합니다.
type UserLoadingState =
LoadingState<User>;const loading:
UserLoadingState = {
id: false,
name: true,
email: false,
isActive: false,
};각 필드를 별도로 불러오는 화면에서 사용할 수 있습니다.
User 타입
↓
키는 그대로 유지
number/string/boolean
↓
모두 boolean으로 변환
결과
↓
필드별 로딩 상태35. 값에 `null`을 추가하기
모든 속성에 `null` 가능성을 추가할 수도 있습니다.
type Nullable<T> = {
[Key in keyof T]:
T[Key] | null;
};type NullableUser =
Nullable<User>;결과:
type NullableUser = {
id: number | null;
name: string | null;
email: string | null;
isActive:
boolean | null;
};API 데이터를 아직 불러오지 않은 상태를 표현하는 등의 용도로 사용할 수 있습니다.
const emptyUser:
NullableUser = {
id: null,
name: null,
email: null,
isActive: null,
};36. Promise 타입으로 변환하기
모든 속성의 값을 비동기 결과로 만들 수도 있습니다.
type AsyncProperties<T> = {
[Key in keyof T]:
Promise<T[Key]>;
};적용:
type AsyncUser =
AsyncProperties<User>;개념적인 결과:
type AsyncUser = {
id: Promise<number>;
name: Promise<string>;
email: Promise<string>;
isActive:
Promise<boolean>;
};이처럼 매핑된 타입은 원본 키를 기준으로 전혀 새로운 형태의 타입을 만들어낼 수 있습니다.
37. 함수로 변환하기
각 속성을 해당 값을 반환하는 함수로 바꿔 보겠습니다.
type Getters<T> = {
[Key in keyof T]:
() => T[Key];
};적용:
type UserGetters =
Getters<User>;결과:
type UserGetters = {
id: () => number;
name: () => string;
email: () => string;
isActive:
() => boolean;
};객체를 구현해 보겠습니다.
const getters:
UserGetters = {
id() {
return 1;
},
name() {
return "김타입";
},
email() {
return "type@example.com";
},
isActive() {
return true;
},
};값 저장소가 함수 자판기로 변신했습니다. 🥤
38. 키 이름까지 바꿀 수 있을까?
여기서 TypeScript가 한 단계 더 놀라운 기능을 보여줍니다.
원본:
interface User {
id: number;
name: string;
email: string;
}다음 타입을 만들고 싶다고 가정하겠습니다.
type UserGetters = {
getId: () => number;
getName: () => string;
getEmail: () => string;
};원본 키에 `get`을 붙였습니다.
매핑된 타입에서는 키 재매핑을 사용할 수 있습니다.
type GetterMethods<T> = {
[Key in keyof T
as `get${Capitalize<
string & Key
>}`]:
() => T[Key];
};조금 강력한 주문이 등장했습니다. 🧙
하지만 분해하면 이해할 수 있습니다.
39. 키 재매핑의 `as`
기본 매핑:
[Key in keyof T]기존 키를 그대로 사용합니다.
키를 다른 이름으로 바꾸고 싶으면 `as`를 추가합니다.
[
Key in keyof T
as 새로운키
]예를 들어 모든 키 앞에 `get`을 붙입니다.
type GetterMethods<T> = {
[
Key in keyof T
as `get${Capitalize<
string & Key
>}`
]:
() => T[Key];
};`User`에 적용하면:
type UserGetters =
GetterMethods<User>;결과는 개념적으로:
type UserGetters = {
getId:
() => number;
getName:
() => string;
getEmail:
() => string;
getIsActive:
() => boolean;
};40. `Capitalize`는 무엇인가?
다음 타입을 살펴보겠습니다.
Capitalize<"name">결과:
"Name"Capitalize<"email">결과:
"Email"따라서:
`get${Capitalize<"name">}`결과:
"getName"`Capitalize`는 TypeScript의 문자열 관련 내장 유틸리티 타입입니다.
다음과 같은 것들도 있습니다.
Uppercase<"hello">결과:
"HELLO"Lowercase<"HELLO">결과:
"hello"Uncapitalize<"Hello">결과:
"hello"문자열 타입도 조립할 수 있다는 점이 TypeScript 타입 시스템의 재미있는 부분입니다.
41. Setter 메서드 타입 만들기
이번에는 Setter를 만들어 보겠습니다.
원하는 결과:
type UserSetters = {
setId:
(value: number) => void;
setName:
(value: string) => void;
setEmail:
(value: string) => void;
setIsActive:
(value: boolean) => void;
};매핑된 타입:
type SetterMethods<T> = {
[
Key in keyof T
as `set${Capitalize<
string & Key
>}`
]:
(
value: T[Key]
) => void;
};적용:
type UserSetters =
SetterMethods<User>;각 Setter의 매개변수 타입도 원본 속성 타입에 맞게 자동으로 만들어집니다.
42. 이벤트 이름 타입 만들기
객체의 속성 이름을 이벤트 이름으로 변환할 수도 있습니다.
type ChangeEvents<T> = {
[
Key in keyof T
as `${string & Key}Changed`
]:
(
value: T[Key]
) => void;
};`User`에 적용하면:
type UserEvents =
ChangeEvents<User>;결과:
type UserEvents = {
idChanged:
(value: number) => void;
nameChanged:
(value: string) => void;
emailChanged:
(value: string) => void;
isActiveChanged:
(value: boolean) => void;
};프론트엔드 상태 관리나 이벤트 시스템을 설계할 때 이런 패턴을 활용할 수 있습니다.
TypeScript가 속성 이름을 붙잡고 이벤트 제작 공장으로 끌고 갑니다. 🏭
43. 특정 키를 제거할 수도 있다
키 재매핑에서 결과 키를 `never`로 만들면 해당 키를 제거할 수 있습니다.
다음 타입에서 `id`를 제거해 보겠습니다.
type RemoveId<T> = {
[
Key in keyof T
as Key extends "id"
? never
: Key
]:
T[Key];
};적용:
type UserWithoutId =
RemoveId<User>;결과:
type UserWithoutId = {
name: string;
email: string;
isActive: boolean;
};`id` 키가 사라졌습니다.
진행 과정:
Key = "id"
→ 조건 만족
→ never
→ 키 제거
Key = "name"
→ 유지
Key = "email"
→ 유지`never`가 타입 세계의 삭제 버튼으로도 등장합니다.
10편에서 만났던 그 `never`가 여기서 재취업했습니다.
44. 이것이 `Omit`과 연결된다
TypeScript에는 특정 속성을 제거하는 `Omit` 유틸리티 타입이 있습니다.
type UserWithoutId =
Omit<User, "id">;결과:
{
name: string;
email: string;
isActive: boolean;
}여러 속성도 제거할 수 있습니다.
type PublicUser =
Omit<
User,
"email"
| "isActive"
>;정확한 내부 구현에는 다른 유틸리티 타입들이 함께 사용될 수 있지만, 핵심 아이디어는 다음과 같습니다.
원본 키를 순회
↓
제외할 키 판단
↓
남은 키만 새 타입에 포함매핑된 타입과 조건부 타입이 만나면 이런 타입 변환이 가능합니다.
조건부 타입은 뒤에서 본격적으로 다루게 됩니다.
45. 필요한 키만 고르는 `Pick`
이번에는 반대로 일부 속성만 가져오겠습니다.
type UserSummary =
Pick<
User,
"id" | "name"
>;결과:
type UserSummary = {
id: number;
name: string;
};직접 비슷한 타입을 만들어 볼 수 있습니다.
type MyPick<
T,
K extends keyof T
> = {
[Key in K]:
T[Key];
};사용합니다.
type UserSummary =
MyPick<
User,
"id" | "name"
>;동작 과정을 살펴보겠습니다.
K
→ "id" | "name"
Key in K
→ id와 name만 순회
T[Key]
→ 각 속성 타입 추출결과:
{
id: number;
name: string;
}46. `Pick`은 왜 유용할까?
전체 사용자 타입이 있습니다.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}화면의 간단한 사용자 목록에서는 ID와 이름만 필요합니다.
type UserListItem =
Pick<
User,
"id" | "name"
>;const user:
UserListItem = {
id: 1,
name: "김타입",
};같은 구조를 다시 작성할 필요가 없습니다.
좋지 않은 방식:
interface UserListItem {
id: number;
name: string;
}처음에는 문제없지만 원본 `User.id` 타입이 변경되면 두 곳이 어긋날 수 있습니다.
`Pick`은 원본 타입과의 연결을 유지합니다.
47. 객체 타입의 부분 변형
실무에서는 하나의 원본 타입에서 여러 파생 타입을 만드는 일이 많습니다.
interface User {
readonly id: number;
name: string;
email: string;
password: string;
isActive: boolean;
}사용자 생성:
type CreateUserInput =
Omit<
User,
"id"
| "isActive"
>;사용자 수정:
type UpdateUserInput =
Partial<
Pick<
User,
"name"
| "email"
| "password"
>
>;사용자 목록:
type UserListItem =
Pick<
User,
"id"
| "name"
| "isActive"
>;공개 프로필:
type PublicUser =
Omit<
User,
"password"
>;하나의 원본 타입이 여러 업무 화면에 맞게 변신합니다.
User
├─ CreateUserInput
├─ UpdateUserInput
├─ UserListItem
└─ PublicUser한 명의 배우가 의상만 갈아입고 네 역할을 맡고 있습니다. 🎭
48. `Record`도 매핑된 타입과 연결된다
지난 시간에 배운 `Record`를 다시 살펴보겠습니다.
type Role =
| "admin"
| "manager"
| "user";type RoleLabels =
Record<Role, string>;결과:
type RoleLabels = {
admin: string;
manager: string;
user: string;
};개념적으로 비슷한 타입을 직접 작성할 수 있습니다.
type MyRecord<
K extends PropertyKey,
V
> = {
[Key in K]: V;
};type RoleLabels =
MyRecord<
Role,
string
>;여기에서도 매핑된 타입이 사용됩니다.
Role의 키 하나씩 순회
↓
각 값 타입을 string으로 지정`Record`도 타입 공방에서 만들어진 기성품 중 하나입니다.
49. 주요 유틸리티 타입의 원리 비교
지금까지 배운 개념으로 대표 유틸리티 타입을 이해할 수 있습니다.
`Partial`
type MyPartial<T> = {
[K in keyof T]?:
T[K];
};모든 속성을 선택적으로 만듭니다.
`Required`
type MyRequired<T> = {
[K in keyof T]-?:
T[K];
};모든 선택적 속성을 필수로 만듭니다.
`Readonly`
type MyReadonly<T> = {
readonly [
K in keyof T
]:
T[K];
};모든 속성을 읽기 전용으로 만듭니다.
`Pick`
type MyPick<
T,
K extends keyof T
> = {
[P in K]:
T[P];
};선택한 키만 가져옵니다.
`Record`
type MyRecord<
K extends PropertyKey,
V
> = {
[P in K]:
V;
};키 집합을 값 타입에 매핑합니다.
50. 타입 변환의 방향을 이해하자
매핑된 타입을 볼 때 다음 세 가지를 확인하면 이해하기 쉽습니다.
첫 번째: 어떤 키를 순회하는가?
keyof T또는:
K두 번째: 키 이름을 그대로 사용하는가?
[Key in keyof T]아니면 바꾸는가?
[
Key in keyof T
as 새로운키
]세 번째: 값 타입을 어떻게 만드는가?
원래 값 유지:
T[Key]불리언으로 변경:
boolean함수로 변경:
() => T[Key]`null` 추가:
T[Key] | null이 세 가지만 확인하면 복잡한 매핑 타입도 상당 부분 해석할 수 있습니다.
51. 실습 1: 폼 상태 타입 자동 생성하기
회원가입 폼 타입을 정의합니다.
interface SignupForm {
email: string;
password: string;
userName: string;
age: number;
}각 필드의 변경 여부를 관리하고 싶습니다.
type TouchedFields<T> = {
[Key in keyof T]:
boolean;
};type SignupTouched =
TouchedFields<
SignupForm
>;결과:
type SignupTouched = {
email: boolean;
password: boolean;
userName: boolean;
age: boolean;
};초기 상태를 만듭니다.
const touched:
SignupTouched = {
email: false,
password: false,
userName: false,
age: false,
};이메일 필드를 사용자가 건드렸습니다.
touched.email = true;새로운 폼 필드가 추가되면 `SignupTouched`에도 자동으로 추가됩니다.
52. 실습 2: 폼 오류 타입 자동 생성하기
이번에는 오류 메시지입니다.
type FormErrors<T> = {
[Key in keyof T]?:
string;
};type SignupErrors =
FormErrors<
SignupForm
>;검증 함수를 작성합니다.
function validateSignup(
form: SignupForm
): SignupErrors {
const errors:
SignupErrors = {};
if (
!form.email.includes(
"@"
)
) {
errors.email =
"이메일 형식이 올바르지 않습니다.";
}
if (
form.password.length <
8
) {
errors.password =
"비밀번호는 8자 이상이어야 합니다.";
}
if (
form.userName
.trim()
.length === 0
) {
errors.userName =
"이름을 입력해 주세요.";
}
if (
form.age < 0
) {
errors.age =
"나이를 확인해 주세요.";
}
return errors;
}폼과 오류 객체의 키가 자동으로 연결됩니다.
53. 실습 3: API 응답용 읽기 전용 타입
상품 타입이 있습니다.
interface Product {
id: number;
name: string;
price: number;
stock: number;
}API에서 조회된 데이터는 화면에서 직접 변경하지 못하게 하고 싶습니다.
type ApiResponse<T> =
Readonly<T>;type ProductResponse =
ApiResponse<Product>;const product:
ProductResponse = {
id: 1,
name: "기계식 키보드",
price: 120000,
stock: 10,
};수정하려 하면 오류입니다.
product.price =
100000;조회 데이터와 편집 데이터를 타입으로 분리할 수 있습니다.
54. 실습 4: 업데이트 DTO 만들기
원본 타입:
interface Product {
readonly id: number;
name: string;
price: number;
stock: number;
createdAt: string;
}수정 시에는 `id`, `createdAt`을 수정하면 안 됩니다.
그리고 나머지 속성도 모두 선택적으로 받고 싶습니다.
먼저 수정 가능한 속성만 고릅니다.
type EditableProduct =
Pick<
Product,
"name"
| "price"
| "stock"
>;모든 속성을 선택적으로 만듭니다.
type UpdateProductInput =
Partial<
EditableProduct
>;결과:
type UpdateProductInput = {
name?: string;
price?: number;
stock?: number;
};사용:
const changes:
UpdateProductInput = {
price: 110000,
};ID를 수정하려 하면 오류입니다.
const changes:
UpdateProductInput = {
id: 100,
};55. 실습 5: Getter 타입 자동 생성하기
사용자 타입:
interface User {
id: number;
name: string;
email: string;
}Getter 타입을 정의합니다.
type Getters<T> = {
[
Key in keyof T
as `get${Capitalize<
string & Key
>}`
]:
() => T[Key];
};적용:
type UserGetters =
Getters<User>;객체 구현:
const userGetters:
UserGetters = {
getId() {
return 1;
},
getName() {
return "김타입";
},
getEmail() {
return "type@example.com";
},
};잘못된 반환 타입은 막힙니다.
const userGetters:
UserGetters = {
getId() {
return "1";
},
getName() {
return "김타입";
},
getEmail() {
return "type@example.com";
},
};`getId()`는 `number`를 반환해야 합니다.
56. 실습 6: 변경 이벤트 타입 만들기
상품 타입:
interface Product {
name: string;
price: number;
stock: number;
}변경 이벤트 타입:
type ChangeHandlers<T> = {
[
Key in keyof T
as `${string & Key}Changed`
]?:
(
newValue: T[Key]
) => void;
};적용:
type ProductHandlers =
ChangeHandlers<Product>;결과:
type ProductHandlers = {
nameChanged?:
(newValue: string)
=> void;
priceChanged?:
(newValue: number)
=> void;
stockChanged?:
(newValue: number)
=> void;
};사용:
const handlers:
ProductHandlers = {
priceChanged(
newPrice
) {
console.log(
`새 가격: ${newPrice}`
);
},
stockChanged(
newStock
) {
console.log(
`새 재고: ${newStock}`
);
},
};각 이벤트의 매개변수 타입도 원본 속성 타입을 자동으로 따라갑니다.
57. 자주 발생하는 실수
실수 1. 타입을 직접 복사해서 다시 작성하기
interface User {
id: number;
name: string;
}type UserId =
number;원본 타입이 바뀌면 어긋날 수 있습니다.
type UserId =
User["id"];원본에서 직접 가져오는 편이 안전합니다.
실수 2. 존재하지 않는 키로 접근하기
type PasswordType =
User["password"];`User`에 해당 속성이 없다면 오류가 발생합니다.
실수 3. `T[keyof T]`가 객체 타입이라고 생각하기
type UserValue =
User[keyof User];결과는 객체 구조가 아닙니다.
각 속성 값 타입의 유니언입니다.
number
| string
| boolean실수 4. `Partial<T>`가 일부 속성을 삭제한다고 생각하기
type OptionalUser =
Partial<User>;속성을 타입에서 제거하는 것이 아닙니다.
모든 속성을 선택적으로 만듭니다.
id?: number;
name?: string;실수 5. `Readonly<T>`가 런타임 객체를 동결한다고 생각하기
type ReadonlyUser =
Readonly<User>;TypeScript 타입 검사 단계에서 수정을 막습니다.
실행 중 객체를 자동으로 `Object.freeze()` 하는 것은 아닙니다.
실수 6. 매핑된 타입에서 값 타입을 무조건 `T[Key]`로 써야 한다고 생각하기
다음처럼 완전히 다른 타입으로 바꿀 수도 있습니다.
type Flags<T> = {
[Key in keyof T]:
boolean;
};실수 7. 키 재매핑을 값 변환으로 생각하기
as `get${...}`이 `as`는 실제 JavaScript 객체의 키를 바꾸지 않습니다.
새로운 타입의 키 이름을 만드는 것입니다.
실수 8. 유틸리티 타입을 무조건 중첩하기
다음 타입은 동작할 수 있지만 읽기 어려워질 수 있습니다.
type Something =
Partial<
Readonly<
Pick<
Omit<
User,
"email"
>,
"id" | "name"
>
>
>;복잡해지면 단계별 타입으로 분리합니다.
type PublicUser =
Omit<
User,
"email"
>;type UserSummary =
Pick<
PublicUser,
"id" | "name"
>;type OptionalUserSummary =
Partial<
Readonly<
UserSummary
>
>;타입도 너무 겹쳐 입으면 코트를 벗다가 봄이 옵니다. 🧥
58. 인덱스드 액세스 타입 선택 공식
다음 상황에서는 인덱스드 액세스 타입을 고려합니다.
기존 타입의 특정 속성 타입이 필요한가?
type UserName =
User["name"];여러 속성 값 타입이 필요한가?
type Value =
User[
"id" | "name"
];모든 속성 값 타입이 필요한가?
type UserValue =
User[keyof User];배열 요소 타입이 필요한가?
type Item =
Users[number];실제 배열에서 요소 타입을 만들고 싶은가?
type Item =
typeof users[number];중첩 객체의 일부 타입이 필요한가?
type Address =
User["profile"]["address"];59. 매핑된 타입 선택 공식
다음 상황에서는 매핑된 타입을 고려합니다.
기존 객체 타입의 모든 키를 유지하면서 속성 규칙을 바꾸려는가?
type Optional<T> = {
[K in keyof T]?:
T[K];
};모든 속성 값을 같은 타입으로 바꾸려는가?
type Flags<T> = {
[K in keyof T]:
boolean;
};모든 속성에 `readonly`를 추가하려는가?
type Locked<T> = {
readonly [
K in keyof T
]:
T[K];
};모든 선택적 속성을 필수로 만들려는가?
type Complete<T> = {
[K in keyof T]-?:
T[K];
};새로운 키 이름을 자동 생성하려는가?
type Getters<T> = {
[
K in keyof T
as `get${Capitalize<
string & K
>}`
]:
() => T[K];
};60. 미니 퀴즈
문제 1
다음 타입의 결과는 무엇일까요?
interface User {
id: number;
name: string;
}
type UserName =
User["name"];정답
string문제 2
다음 타입의 결과는 무엇일까요?
type UserInfo =
User["id" | "name"];정답
number | string문제 3
다음 타입은 무엇을 의미할까요?
User[keyof User]정답
`User`의 모든 속성 값 타입을 유니언으로 가져옵니다.
문제 4
다음 타입의 결과는 무엇일까요?
type Items =
string[];
type Item =
Items[number];정답
string문제 5
다음 타입은 무엇을 만들까요?
type Optional<T> = {
[K in keyof T]?:
T[K];
};정답
`T`의 모든 속성을 선택적으로 만든 새로운 객체 타입을 만듭니다.
문제 6
`?`를 제거하려면 어떤 수정자를 사용할까요?
정답
-?예:
type RequiredType<T> = {
[K in keyof T]-?:
T[K];
};문제 7
`readonly`를 제거하려면 어떻게 할까요?
정답
-readonly예:
type Mutable<T> = {
-readonly [
K in keyof T
]:
T[K];
};문제 8
다음 타입의 결과는 무엇일까요?
type Flags<T> = {
[K in keyof T]:
boolean;
};정답
원본 타입의 키는 그대로 유지하면서 모든 값 타입을 `boolean`으로 변경합니다.
문제 9
다음 코드에서 `Partial`의 역할은 무엇일까요?
type UpdateUserInput =
Partial<User>;정답
`User`의 모든 속성을 선택적으로 만들어 일부 속성만 전달할 수 있게 합니다.
문제 10
다음 타입에서 사용된 `as`의 역할은 무엇일까요?
type Getters<T> = {
[
K in keyof T
as `get${Capitalize<
string & K
>}`
]:
() => T[K];
};정답
원본 키를 새로운 이름으로 재매핑합니다.
예를 들어 `name`을 `getName`으로 바꿉니다.
61. 핵심 정리
특정 속성 타입 가져오기
type UserName =
User["name"];여러 속성 값 타입 가져오기
type UserInfo =
User[
"id" | "name"
];모든 값 타입 가져오기
type UserValue =
User[keyof User];배열 요소 타입 가져오기
type Item =
Users[number];실제 배열에서 요소 타입 추출
type User =
typeof users[number];매핑된 타입 기본 형태
type Copy<T> = {
[K in keyof T]:
T[K];
};모든 속성을 선택적으로
type Optional<T> = {
[K in keyof T]?:
T[K];
};TypeScript 내장 타입:
Partial<T>모든 속성을 필수로
type Complete<T> = {
[K in keyof T]-?:
T[K];
};TypeScript 내장 타입:
Required<T>모든 속성을 읽기 전용으로
type Locked<T> = {
readonly [
K in keyof T
]:
T[K];
};TypeScript 내장 타입:
Readonly<T>일부 속성 선택
Pick<
User,
"id" | "name"
>일부 속성 제외
Omit<
User,
"password"
>키와 값을 매핑
Record<
Role,
string
>62. 마무리
이제 우리는 객체 타입을 단순히 작성하는 수준에서 벗어나기 시작했습니다.
처음에는 직접 타입을 만들었습니다.
interface User {
id: number;
name: string;
email: string;
}`keyof`를 사용해 키를 꺼냈습니다.
type UserKey =
keyof User;이번에는 인덱스드 액세스 타입으로 값의 타입을 꺼냈습니다.
type UserName =
User["name"];모든 값 타입을 가져올 수도 있습니다.
type UserValue =
User[keyof User];그리고 매핑된 타입으로 객체 타입 전체를 순회하면서 새로운 타입을 만들었습니다.
type OptionalUser = {
[K in keyof User]?:
User[K];
};모든 속성을 읽기 전용으로 만들 수도 있습니다.
type ReadonlyUser = {
readonly [
K in keyof User
]:
User[K];
};값 타입을 완전히 바꿀 수도 있습니다.
type UserFlags = {
[K in keyof User]:
boolean;
};심지어 키 이름까지 바꿀 수 있습니다.
type UserGetters = {
[
K in keyof User
as `get${Capitalize<
string & K
>}`
]:
() => User[K];
};이번 편에서 가장 중요한 흐름은 다음과 같습니다.
객체 타입
↓
keyof
키 추출
↓
T[K]
값 타입 추출
↓
[K in keyof T]
키 순회
↓
새로운 객체 타입 생성이제 TypeScript 타입은 단순한 이름표가 아닙니다.
기존 타입을 재료로 새로운 타입을 생산하는 작은 프로그래밍 언어처럼 보이기 시작합니다.
그리고 우리가 평소 편하게 사용하던 다음 타입들도:
Partial<T>
Required<T>
Readonly<T>
Pick<T, K>
Record<K, V>갑자기 마법이 아니라 이해 가능한 기계 장치로 보이기 시작합니다.
TypeScript 타입 공방의 벽을 뜯어보니 안에서는 `keyof`, `T[K]`, 매핑된 타입이 톱니바퀴처럼 돌아가고 있었습니다. ⚙️
63. 다음 편 예고
지금까지는 객체 타입을 변환하는 방법을 배웠습니다.
type OptionalUser =
Partial<User>;type UserSummary =
Pick<
User,
"id" | "name"
>;type PublicUser =
Omit<
User,
"password"
>;그런데 TypeScript에는 이런 변환 작업을 위해 이미 준비된 유틸리티 타입이 훨씬 더 많습니다.
예를 들어 두 타입 중 특정 값만 제거할 수 있습니다.
type Status =
| "idle"
| "loading"
| "success"
| "error";
type ActiveStatus =
Exclude<
Status,
"idle"
>;결과:
"loading"
| "success"
| "error"반대로 원하는 값만 남길 수도 있습니다.
type FinalStatus =
Extract<
Status,
"success"
| "error"
>;`null`과 `undefined`만 제거할 수도 있습니다.
type Name =
string
| null
| undefined;
type SafeName =
NonNullable<Name>;함수의 반환 타입을 자동으로 추출할 수도 있습니다.
function createUser() {
return {
id: 1,
name: "김타입",
};
}type User =
ReturnType<
typeof createUser
>;함수의 매개변수 타입도 꺼낼 수 있습니다.
type CreateUserParams =
Parameters<
typeof createUser
>;TypeScript는 우리가 직접 만들던 타입 변환 도구를 이미 한 상자 가득 준비해 두었습니다. 🧰
다음 이야기
[TypeScript 완전정복 #17] 타입을 직접 다시 만들지 마세요 | TypeScript 유틸리티 타입 완벽 이해하기
- `Partial<T>`는 언제 사용할까요?
- `Required<T>`는 무엇을 할까요?
- `Readonly<T>`와 `readonly`는 어떻게 다를까요?
- `Pick<T, K>`로 필요한 속성만 어떻게 가져올까요?
- `Omit<T, K>`로 특정 속성을 제거하려면 어떻게 할까요?
- `Record<K, V>`를 실무에서는 어떻게 활용할까요?
- `Exclude<T, U>`와 `Extract<T, U>`는 무엇일까요?
- `NonNullable<T>`는 언제 사용할까요?
- `ReturnType<T>`으로 함수 결과 타입을 추출하려면 어떻게 할까요?
- `Parameters<T>`로 함수 매개변수 타입을 꺼낼 수 있을까요?
- 유틸리티 타입을 여러 개 조합하면 무엇이 가능할까요?
- 유틸리티 타입을 너무 많이 중첩하면 왜 읽기 어려워질까요?
다음 편에서는 지금까지 직접 조립했던 타입 변환 도구들을 TypeScript가 미리 만들어 둔 유틸리티 타입 공구함에서 하나씩 꺼내보겠습니다. 🧰
