1. 타입 조작이란
타입을 조작한다는 것은 기본 타입이나 별칭 또는 인터페이스로 만든 원래 존재하던 타입들을 상황에 따라 유동적으로 다른 타입으로 변환하는 것이다.
2. 인덱스드 엑세스 타입
인덱스드 엑세스 타입은 인덱스를 이용해 다른 타입 내의 특정 프로퍼티의 타입을 추출하는 타입이다. 인덱스드 엑세스 타입은 객체, 배열, 튜플에 사용할 수 있다. 복잡하고 큰 타입으로 부터 잘게 나눈 것의 타입을 뽑아낼 수 있어 실무에서 자주 쓰인다.
1) 객체 프로퍼티의 타입 추출하기
다음과 같은 게시글을 표현하는 객체 타입이 있다고 가정해보자.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number; // 추가
};
}
function printAuthorInfo(author: Post["author"]) {
console.log(`${author.id} - ${author.name}`);
}
(...)
Post["author"]는 Post 타입으로부터 author 프로퍼티의 타입을 추출한다. 그 결과 author 매개변수의 타입은 {id : number, name: string, age:number}가 된다.
이때 대괄호 속에 들어가는 String Literal 타입인 “author” 를 인덱스 라고 부른다. 그래서 인덱스를 이용해 특정 타입에 접근하다고 하여 인덱스드 엑세스 타입이라 부른다.
다음과 같이 인덱스를 중첩하여 사용할 수도 있다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}
function printAuthorInfo(author: Post["author"]['id']) {
// author 매개변수의 타입은 number 타입이 됨
console.log(`${author.id} - ${author.name}`);
}
// Error
// Property 'id' does not exist on type 'number'.
// Property 'name' does not exist on type 'number'.
주의할 점은 인덱스에는 값이 아니라 타입만 들어갈 수 있다. 따라서 다음과 같이 “author”를 문자열 값으로 다른 변수에 저장하고 다음과 같이 인덱스로 사용하려고 하면 오류가 발생한다.
const authorKey = "author";
function printAuthorInfo(author: Post[authorKey]) { // ❌
console.log(`${author.id} - ${author.name}`);
}
또 한가지 주의할 점은 인덱스에 존재하지 않는 프로퍼티 이름을 쓰면 오류가 발생한다.
function printAuthorInfo(author: Post["what"]) { // ❌
console.log(`${author.id} - ${author.name}`);
}
2) 배열 요소의 타입 추출하기
앞서 만든 Post 타입을 다음과 같이 PostList 배열 타입으로 수정한다.
type PostList = {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}[];
인덱스드 엑세스 타입을 이용해 다음과 같이 이 PostList 배열 타입에서 하나의 요소의 타입만 뽑아올 수 있다.
PostList[number]는 PostList 배열 타입으로부터 요소의 타입을 추출하는 인덱스드 엑세스 타입이다. 이렇듯 배열의 요소 타입을 추출할 때에는 인덱스에 number 타입을 넣어주면 된다.
인덱스에 다음과 같이 Number Literal 타입을 넣어도 숫자와 관계없이 모두 Number 타입을 넣은 것과 동일하게 동작한다.
const post: PostList[number] = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "이정환",
age: 27,
},
};
// 또는
const post: PostList[0] = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "이정환",
age: 27,
},
};
3) 튜플의 요소 타입 추출하기
type Tup = [number, string, boolean];
type Tup0 = Tup[0];
// number
type Tup1 = Tup[1];
// string
type Tup2 = Tup[2];
// boolean
type Tup3 = Tup[number]
// number | string | boolean
한가지 주의할 점은 튜플 타입에 인덱스드 엑세스 타입을 사용할 때 인덱스에 number 타입을 넣으면 마치 튜플을 배열 처럼 인식해 배열 요소의 타입을 추출(예시에서는 모든 요소의 유니온 타입)하게 된다.
3. keyof & typeof 연산자
1) keyof
keyof 연산자는 객체 타입으로부터 프로퍼티의 모든 key들을 String Literal Union 타입으로 추출하는 연산자이다.
interface Person {
name: string;
age: number;
location: string; // 추가
}
function getPropertyKey(person: Person, key: keyof Person) {
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
keyof 연산자는 타입의 모든 프로퍼티 key를 String Literal Union 타입으로 추출한다. 따라서 keyof Person의 결과값은 “name” | “age” | “location”이 된다.
한가지 주의할 점은 keyof 연산자는 오직 타입에만 적용할 수 있어 다음과 같이 값과 함께 사용하려고 하면 오류가 발생한다.
(...)
function getPropertyKey(person: Person, key: keyof person) { // ❌
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
2) typeof
typeof 연산자는 자바스크립트에서 특정 값의 타입을 문자열로 반환하는 연산자이다. 그러나 다음과 같이 타입을 정의할 때 사용하면 특정 변수의 타입을 추론하는 기능도 가지고 있다.
type Person = typeof person;
// 결과
// {name: string, age: number, location:string}
(...)
keyof 연산자를 다음과 같이 사용할 수 있다.
(...)
function getPropertyKey(person: Person, key: keyof typeof person) {
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
4. 맵드 타입
맵드 타입은 기존의 객체 타입을 기반으로 새로운 객체 타입을 만드는 타입 조작 기능이다. interface에서는 사용이 불가하여 type 에서만 사용이 가능하다.
interface User {
id: number;
name: string;
age: number;
}
function fetchUser(): User {
return {
id: 1,
name: "이정환",
age: 27,
};
}
다음으로는 유저 정보가 서버에 저장되어 있다고 가정하고, 한명의 유저 정보를 불러오는 기능을 함수로 만든다. 그리고 한 명의 유저 정보를 수정하는 기능도 만든다.
interface User {
id: number;
name: string;
age: number;
}
function fetchUser(): User {
(...)
}
function updateUser(user: User) {
// ... 유저 정보 수정 기능
}
updateUser 함수는 수정된 유저 객체를 받아 유저 정보를 수정한다. 따라서 유저 정보를 수정 하려면 다음과 같이 이 함수를 호출하고 여러개의 정보 중 수정하고 싶은 프로퍼티만 전달 해 주면 된다. 그런데 updateUser 함수의 파라미터 타입이 User 타입으로 되어 있어서 수정하고 싶은 프로퍼티만 골라서 보낼 수 없는 상황이다.
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = {
[key in "id" | "name" | "age"]?: User[key];
};
(...)
PartialUser 타입을 맵드 타입을 이용해 아까와 동일한 타입으로 정의했다.
[key in “id” | “name” | “age”] 는 이 객체 타입은 key가 한번은 id, 한번은 name, 한번은 age가 된다는 뜻이다. 따라서 다음과 같이 3개의 프로퍼티를 갖는 객체 타입으로 정의된다.
- key가 “id” 일 때 → id : User[id] → id : number
- key가 “name”일 때 → name : User[user] → name : string
- key가 “age”일 때 → age : User[age] → age : number
여기에 대 괄호 뒤에 선택적 프로퍼티를 의미하는 물음표(?) 키워드가 붙어있으므로 모든 프로퍼티가 선택적 프로퍼티가 되어 결론적으로 이 타입은 다음과 같은 타입이 된다.
{
id?: number;
name?: string;
age?: number;
}
이렇듯 맵드 타입을 이용하면 단 한줄의 코드 만으로 중복 없이 기존 타입을 변환할 수 있다. keyof 연산자를 이용해 한번 더 업그레이드 하면 다음과 같다.
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = {
[key in keyof User]?: User[key];
};
(...)
맵드 타입을 이용해 모든 프로퍼티가 읽기 전용 프로퍼티가 된 타입을 만들면 다음과 같다.
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = {
[key in keyof User]?: User[key];
};
type ReadonlyUser = {
readonly [key in keyof User]: User[key];
};
(...)
5. 템플릿 리터럴 타입
템플릿 리터럴 타입은 타입 조작 기능들 중 가장 단순한 기능으로 템플릿 리터럴을 이용해 특정 패턴을 갖는 String 타입을 만드는 기능이다.
type Color = "red" | "black" | "green";
type Animal = "dog" | "cat" | "chicken";
type ColoredAnimal = `red-dog` | 'red-cat' | 'red-chicken' | 'black-dog' ... ;
Color와 Animal은 각각 3개의 String Literal 타입으로 이루어진 Union 타입이다. 그리고 ColoredAnimal은 Color와 Animal을 조합해 만들 수 있는 모든 가지수의 String Literal 타입으로 이루어진 Union 타입이다.
Color나 Animal 타입에 String Literal 타입이 추가되어 경우의 수가 많아질 수록 ColoredAnimal 타입에 추가해야 하는 타입이 점점 많아지게 된다. 이럴 때 템플릿 리터럴 타입을 이용하면 좋다.
type ColoredAnimal = `${Color}-${Animal}`;
'온라인 강의(유데미, 인프런 등) > 한 입 크기로 잘라먹는 타입스크립트(인프런)' 카테고리의 다른 글
[한 입 크기로 잘라먹는 타입스크립트] 유틸리티 타입 (0) | 2023.06.21 |
---|---|
[한 입 크기로 잘라먹는 타입스크립트] 조건부 타입 (0) | 2023.06.20 |
[한 입 크기로 잘라먹는 타입스크립트] 제네릭 (0) | 2023.06.19 |
[한 입 크기로 잘라먹는 타입스크립트] 인터페이스 (0) | 2023.06.19 |
[한 입 크기로 잘라먹는 타입스크립트] 함수 타입 (0) | 2023.06.17 |