
먼저 타입을 조작한다는 것은 기본 타입이나 별칭 또는 인터페이스로 만든 원래 존재하던 타입들을 상황에 따라 유동적으로 다른 타입으로 변환하는 타입스크립트의 기능이다.
제네릭, 조건부 타입도 타입 조작에 포함된다.
인덱스를 이용해 다른 타입내의 특정 프로퍼티의 타입을 추출하는 타입
인덱스드 엑세스 타입은 객체, 배열, 튜플에 사용할 수 있다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
};
}
const post: Post = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "챙",
},
};
이 게시글에서 작성자의 이름과 아이디를 붙여서 출력하는 어떤 함수를 만든다면 아래와 같다.
function printAuthorInfo(author: { id: number; name: string }) {
console.log(`${author.id} - ${author.name}`);
}
그런데 매개변수의 타입을 이렇게 정의하면 나중에 Post 타입의 author 프로퍼티의 타입이 다음과 같이 수정되면 매개변수의 타입도 그때 마다 계속 수정해줘야 하는 불편함이 존재한다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number; // 추가
};
}
function printAuthorInfo(author: { id: number; name: string, age: number }) {
// age 프로퍼티도 추가
console.log(`${author.id} - ${author.name}`);
}
(...)
이럴때 인덱스드 엑세스 타입을 이용할 수 있다.
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” 를 인덱스 라고 부르고 인덱스를 이용해 특정 타입에 접근하다고 하여 인덱스드 엑세스 타입이라 부릅니다.
주의할 점은 인덱스에는 값이 아니라 타입만 들어갈 수 있다. 따라서 다음과 같이 “author”를 문자열 값으로 다른 변수에 저장하고 다음과 같이 인덱스로 사용하려고 하면 오류가 발생한다.
const authorKey = "author"; // 값이 "author"
function printAuthorInfo(author: Post[authorKey]) { // ❌
console.log(`${author.id} - ${author.name}`);
}
또 한가지 주의할 점은 인덱스에 존재하지 않는 프로퍼티 이름을 쓰면 오류가 발생한다.
function printAuthorInfo(author: Post["what"]) { // ❌
console.log(`${author.id} - ${author.name}`);
}
다음과 같이 인덱스를 중첩하여 사용할 수도 있습니다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}
function printAuthorInfo(authorId: Post["author"]["id"]) {
console.log(authorId);
}
인덱스드 엑세스 타입은 객체 프로퍼티의 타입 뿐만 아니라 특정 배열의 요소 타입을 추출하는데에도 이용할 수 있다.
type PostList = {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}[];
const post: PostList[number] = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "이정환",
age: 27,
},
};
PostList[number]는 PostList 배열 타입으로부터 요소의 타입을 추출하는 인덱스드 엑세스 타입이 된다. 이렇듯 배열의 요소 타입을 추출할 때에는 인덱스에 number 타입을 넣어주면 된다.
또 인덱스에 다음과 같이 Number Literal 타입을 넣어도 된다. 숫자와 관계없이 모두 Number 타입을 넣은 것과 동일하게 동작하게 된다.
const post: PostList[0] = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "이정환",
age: 27,
},
};
객체프로퍼티의 타입 추출과도 함께 사용할 수 있다.
function printAuthorInfo(authorId: PostList[number]["author"]["id"]) {
console.log(authorId);
}
마지막으로 튜플의 각 요소들의 타입또한 다음과 같이 인덱스드 엑세스 타입으로 쉽게 추출할 수 있다.
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 타입을 넣으면 마치 튜플을 배열 처럼 인식해 배열 요소의 타입을 추출하게 된다.
keyof 연산자는 객체 타입으로부터 프로퍼티의 모든 key들을 String Literal Union 타입으로 추출하는 연산자이다.
interface Person {
name: string;
age: number;
}
function getPropertyKey(person: Person, key: "name" | "age") {
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
Person 객체 타입을 정의하고 해당 타입을 갖는 변수를 하나 선언한다.
그리고 getPropertyKey함수를 만듭니다. 이 함수는 두개의 매개변수가 있으며 두번째 매개변수 key에 해당하는 프로퍼티의 값을 첫번째 매개변수 person에서 꺼내 반환한다.
이때 key의 타입을 “name” | “age”로 정의했는데 이렇게 정의하면 Person 타입에 새로운 프로퍼티가 추가되거나 수정될 때 마다 이 타입도 계속 바꿔줘야 할 것이다.
이럴 때 다음과 같이 Keyof 연산자를 이용하면 좋다.
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 연산자는 위와 같이 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,
};
typeof 연산자는 자바스크립트에서 특정 값의 타입을 문자열로 반환하는 연산자인데, 다음과 같이 타입을 정의할 때 사용하면 특정 변수의 타입을 추론하는 기능도 가지고 있다.
type Person = typeof person;
// 결과
// {name: string, age: number, location:string}
function getPropertyKey(person: Person, key: keyof typeof person) {
return person[key];
}
const person: Person = {
name: "이정환",
age: 27,
};
keyof typeof: 이 객체의 키 값들만 허용하는 타입을 만들 때 사용
keyof : 자바스크립트의 객체에서 key 만 뽑아서 리터럴 타입으로 반환
typeof : 자바스크립트 객체(값)를 타입스크립트 타입으로 변환
// 1. 이미 타입인 경우
interface User {
name: string;
age: number;
}
type UserKeys = keyof User; // "name" | "age"
// 2. 값(객체)인 경우
const userObj = {
name: 'Kim',
age: 20
};
// type ObjKeys = keyof userObj; // ❌ userObj는 '값'이지 '타입'이 아님
type ObjKeys = keyof typeof userObj; // ✅ "name" | "age"
| 대상 | 사용 문법 | 설명 |
|---|---|---|
| 인터페이스 / 타입 별칭 | keyof Type | 이미 타입이므로 바로 keyof사용가능 |
| 변수 / 객체 (const, let) | keyof typeof obj | 값(Value)이므로 typeof로 타입으로 변환 후 keyof 사용가능 |
자바스크립트의 맵 함수처럼 기존의 객체 타입을 기반으로 새로운 객체 타입을 만드는 타입이다.
맵드 타입은 기존의 객체 타입을 기반으로 새로운 객체 타입을 만드는 타입 조작 기능이다.
interface User {
id: number;
name: string;
age: number;
}
function fetchUser(): User {
return {
id: 1,
name: "이정환",
age: 27,
};
}
function updateUser(user: User) {
// ... 유저 정보 수정 기능
}
updateUser({ // ❌
age: 25
});
updateUser 함수의 매개변수 타입이 User 타입으로 되어 있어서 수정하고 싶은 프로퍼티만 골라서 보낼 수 없는 상황이다.
따라서 다음과 같이 새로운 타입을 만들어 주어야 한다.
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = {
id?: number;
name?: string;
age?: number;
}
function updateUser(user: PartialUser) {
// ... 유저 정보 수정 기능
}
updateUser({ // ✅
age: 25
});
그런데 User 타입과 PartialUser 타입이 지금 서로 중복된 프로퍼티를 정의하고 있다. 따라서 이럴 때 바로 맵드 타입을 이용하면 좋다.
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개의 프로퍼티를 갖는 객체 타입으로 정의된다.
id : User[id] → id : numbername : User[user] → name : stringage : 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];
};
기존의 스트링 리터럴 타입을 기반으로 정해진 패턴의 문자열만 포함하는 템플릿 리터럴 타입
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}`;
타입스크립트는 가능한 모든 경우의 수를 조합하여 다음과 같은 타입을 만들어낸다.
// type ColoredAnimal =
// | "red-dog" | "red-cat" | "red-chicken"
// | "black-dog" | "black-cat" | "black-chicken"
// | "green-dog" | "green-cat" | "green-chicken"
이처럼 타입 조작 기능을 잘 활용하면, 하드코딩을 줄이고 변경에 유연하게 대처하는 우아하게 코딩을 할 수 있다.