💡 타입 조작
타입을 조작한다는 것은 기본 타입이나 별칭 또는 인터페이스로 만든 원래 존재하던 타입들을 상황에 따라 유동적으로 다른 타입으로 변환하는 타입스크립트의 강력한 기능입니다. 타입스크립트에서는 제네릭 이외에도 다양한 타입 조작 기능을 제공합니다.
타입을 조작하는 방법에는 다음과 같이 4가지의 방법이 있습니다. 이번 게시물에서는 '인덱스드 엑세스 타입'과 'keyof & typeof 연산자'에 대해 먼저 설명하고 다음 게시물에서 나머지 두가지 방법에 대해 다루겠습니다.
⓵ 첫번째로 살펴볼 타입 조작 기능은 객체, 배열, 튜플 타입으로부터 특정 프로퍼티나 특정 요소의 타입만 추출하는 인덱스드 액세스 타입입니다.
⓶ 두번째로는 객체 타입으로부터 해당 타입 내에 정의된 프로퍼티의 키들을 유니온 타입으로 추출하는 Keyof 연산자입니다.
⓷ 세번째로는 마치 자바스크립트의 맵 함수처럼 기존의 객체 타입을 기반으로 새로운 객체 타입을 만드는 맵드 타입입니다.
⓸ 네번째로는 기존의 스트링 리터럴 타입을 기반으로 정해진 패턴의 문자열만 포함하는 템플릿 리터럴 타입입니다.
💡 인덱스드 액세스 타입
인덱스드 액세스 타입은 인덱스를 이용하여 다른 타입 내의 특정 프로퍼티의 타입을 추출하는 타입입니다. 인덱스드 액세스 타입은 객체, 배열, 튜플에 사용할 수 있으니 순서대로 살펴보겠습니다.
• 객체 프로퍼티의 타입 추출하기
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
};
}
const post: Post = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "수현",
},
};
위 코드는 게시글을 표현하는 객체 타입(Post)과 게시글 변수(post)입니다.
function printAuthorInfo(author: { id: number; name: string }) {
console.log(`${author.id} - ${author.name}`);
}
이 게시글에서 작성자의 이름과 아이디를 붙여서 출력하는 어떤 함수가 필요하다면 printAuthorInfo와 같은 함수를 만들어주면 됩니다. 하지만 매개변수의 타입을 이와 같이 정의하면 불편함이 생길 수 있습니다.
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}`);
}
함수의 매개변수를 지정한 이후에 이와 같이 나중에 Post 타입의 author 프로퍼티의 타입이 다음과 같이 수정하면 함수의 매개변수의 타입도 그때마다 계속 수정해주어야한다는 번거로움이 발생합니다. 이럴 때에는 인덱스드 액세스 타입을 이용하여 Post에서 author 프로퍼티의 타입을 추출하여 사용하면 편리해집니다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number; // 추가
};
}
// 인덱스드 액세스 타입: Post["author"]
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"를 인덱스라고 부릅니다. 그래서 인덱스를 이용하여 특정 타입에 접근한다는 의미로 인덱스드 액세스 타입이라고 부릅니다.
const key = "author"; // ❌
function printAuthorInfo(author: Post[key]) { // ❌
// index에는 타입만 들어올 수 있다. (존재하지 않는 프로퍼티도 안됨)
console.log(`${author.name}-${author.id}`);
}
function printAuthorInfo(author: Post["what"]) { // ❌
console.log(`${author.id} - ${author.name}`);
}
하지만 다음과 같이 "author"를 저장하고 있는 변수 key를 직접 대괄호에 넣는 것을 불가능하며 오직 타입만 들어갈 수 있습니다. 또한, 인덱스에 존재하지 않는 프로퍼티 이름을 사용하면 오류가 발생합니다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}
function printAuthorInfo(author: Post["author"]["id"]) {
console.log(`${author.name}-${author.id}`);
}
그리고 다음과 같이 인덱스를 중첩하여 Post["author"]["id"] 와 같이 작성할 수도 있습니다. 이러한 상태에서는 author 매개변수의 타입은 author 프로퍼티의 id에 해당하는 타입인 number이 됩니다.
• 배열 요소의 타입 추출하기
type PostList = {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}[];
다음과 같은 PostList 라는 배열 타입에서 인덱스드 액세스 타입을 이용하여 하나의 요소의 타입만을 뽑아올 수도 있습니다.
const post: PostList[number] = {
title: "게시글 제목",
content: "게시글 본문 ",
author: {
id: 1,
name: "수현",
age: 27,
},
};
PostList[number]는 PostList 배열 타입으로부터 요소의 타입을 추출하는 인덱스드 액세스 타입입니다. 따라서 배열의 요소 타입을 추출할 때에는 인덱스에 number 타입을 넣어주면 됩니다.
const post: PostList[0] = {
(...)
}
또한 다음과 같이 인덱스에 Number Literal 타입을 넣어도 되며, Number 타입을 넣은 것과 동일하게 동작합니다.
• 튜플의 요소 타입 추출하기
type Tup = [number, string, boolean];
type Tup0 = Tup[0]; // number
type Tup1 = Tup[1]; // string
type Tup2 = Tup[2]; // boolean
type TupNum = Tup[number]; // 세 타입의 유니온 타입으로 추출한다. (number | string | boolean)
type Tup3 = Tup[3]; // ❌ 존재하지 않는 인덱스의 타입을 추출하려고 하면 오류가 발생한다.
Tup0, Tup1, Tup2는 Tup 타입의 각각의 인덱스가 저장되며, Tup3에는 Tup 타입의 3번째 인덱스값을 저장하려했는데 배열에 없는 인덱스로 오류가 발생합니다. 또한, TupNum에는 Tup 타입의 number 타입을 인덱스에 저장하고자 하였는데 이러한 경우에는 튜플을 배열처럼 인식하여 배열 요소의 타입을 추출하게 되므로 모든 타입이 합쳐진 유니온 타입으로 추출됩니다.
💡 keyof 연산자
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: "shyunu",
age: 27,
};
Person 객체 타입을 정의하고 해당 타입을 갖는 변수 person 을 하나 정의하였습니다. 그리고 gerPropertyKey 함수를 만들었습니다. 이 함수는 두개의 매개변수를 받고 두번째 매개변수 key에 해당하는 프로퍼티의 값을 첫번째 매개변수 person에서 꺼내서 반환합니다.
interface Person {
name: string;
age: number;
location: string; // 추가
}
function getPropertyKey(person: Person, key: "name" | "age" | "location") {
return person[key];
}
초기에는 key의 타입을 "name" | "age"로 정의하였는데 이렇게 직접 타입을 정의하면 불편함이 생깁니다. 다음과 같이 location이라는 프로퍼티가 추가된 상태라면 함수의 매개변수에도 key의 타입을 또 추가해주어야합니다. 이렇게 매번 매개변수의 타입을 바꿔줘야 하면 함수가 많아질수록 더 불편해지는데, 이러한 상황에서 Keyof 연산자를 사용하면 좋습니다.
interface Person {
name: string;
age: number;
location: string; // 추가
}
// keyof 연산자 사용
function getPropertyKey(person: Person, key: keyof Person) {
return person[key];
}
Keyof 연산자는 다음과 같이 keyof 타입 형태로 사용되며 타입의 모든 프로퍼티 key를 String Literal Union 타입으로 추출합니다. 따라서 keyof Person의 결과값은 "name" | "age" | "location" 이 됩니다.
function getPropertyKey(person: Person, key: keyof person) { // ❌
return person[key];
}
const person: Person = {
name: "shyunu",
age: 27,
};
하지만 다음과 같이 keyof 연산자에 변수 person을 사용하는 것은 불가능하며, 오직 타입에만 적용할 수 있는 연산자입니다.
✱ Typeof와 Keyof 함께 사용하기
type Person = typeof person;
const person = {
name: "shyunu",
age: 27,
};
typeof 연산자는 자바스크립트에서 특정 값의 타입을 문자열로 반환하는 연산자입니다. 그러나 다음과 같이 타입을 정의할 때 typeof 연산자를 사용하면 특정 변수의 타입을 추론하는 기능을 하기도 합니다. 현재 변수 person의 프로퍼티와 해당 타입을 Person에도 똑같이 저장됩니다.
type Person = typeof person;
function getPropertyKey(person: Person, key: keyof typeof person) {
return person[key];
}
이러한 특징을 사용하면 keyof 연산자를 다음과 같이 사용할 수 있습니다. typeof person을 Person타입에 저장하였으니까 매개변수 key의 타입에 keyof Person 이라는 표현과 같다고 볼 수 있습니다.
key: keyof Person = key: keyof typeof person
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 조건부 타입 이해하기: 제네릭, 분산적 조건부, infer (0) | 2024.12.23 |
---|---|
[ TypeScript ] 타입 조작하기 2 - 맵드 타입, 템플릿 리터럴 타입 (0) | 2024.12.23 |
[ TypeScript ] 제네릭으로 확장하기: 인터페이스, 클래스, 프로미스 (0) | 2024.12.22 |
[ TypeScript ] 제네릭 이해하기: 타입 변수와 메서드 타입 정의(map, forEach) (1) | 2024.12.22 |
[ TypeScript ] 클래스 이해하기: JavaScript 차이점, 인터페이스, 그리고 접근 제어자 (0) | 2024.12.22 |