타입스크립트 타입 조작하기
TIL 23. 12. 20
타입 조작
타입을 조작한다는 것은 기본 타입이나 별칭 또는 인터페이스로 만든 원래 존재하던 타입들을 상황에 따라 유동적으로 다른 타입으로 변환하는 타입스크립트의 강력하고도 독특한 기능이다. 타입스크립트에서는 타입을 조작하는 여러 가지 조작 기능을 제공한다. 하나씩 살펴보자.
인덱스드 엑세스 타입
인덱스트 엑세스 타입은 인덱스를 이용해 다른 타입 내의 특정 프로퍼티의 타입을 추출하는 타입이다. 대괄호 속에 들어가는 인덱스를 이용해 특정 타입에 접근한다고 하여 붙여진 이름이다. 인덱싱된 타입은 그 자체로 타입이 된다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}
// author: { id: number; name: string; age: number; }
function printAuthorInfo(author: Post["author"]) {
console.log(`${author.id} - ${author.name}`);
}
주의할 점은 인덱스에 들어가는 문자열은 값이 아니라 ‘타입’만 사용 가능하다.
const authorKey = "author";
function printAuthorInfo(author: Post[authorKey]) { // ❌
console.log(`${author.id} - ${author.name}`);
}
인덱스드 엑세스 타입은 객체 프로퍼티의 타입 뿐만 아니라 특정 배열의 요소 타입을 추출하는데에도 이용할 수 있다. 배열에서 하나의 요소의 타입만 추출하기 위해서는 인덱스에 number 타입을 넣어주면 된다.
type PostList = {
title: string;
content: string;
author: {
id: number;
name: string;
age: number;
};
}[];
const post: PostList[number] = {
title: "게시글 제목",
content: "게시글 본문",
author: {
id: 1,
name: "user",
age: 27,
},
};
Keyof 연산자
keyof 연산자는 객체 타입으로부터 프로퍼티의 모든 key들을 String Literal Union 타입으로 추출하는 연산자이다. keyof 연산자는 오직 타입에만 적용할 수 있는 연산자이다.
type Point = { x: number; y: number };
type P = keyof Point;
const pos: Point = {
x: 255,
y: 255,
};
function getAxisPosition(pos: Point, axis: P) {
return pos[axis];
}
keyof 연산자는 위와 같이 keyof 타입
형태로 사용하며 타입
의 모든 프로퍼티 key를 String Literal Union 타입으로 추출한다. keyof Point
의 결과값은 "x" | "y"
가 된다.
Typeof 연산자
typeof 연산자는 특정 값의 타입을 문자열로 반환하는 연산자였다. 그러나 타입을 정의할 때 사용하면 특정 변수의 타입을 추론하도록 동작한다.
const pos = {
x: 255,
y: 255,
};
type Pos = typeof pos; // 아래와 동일
// type Pos = { x: number; y: number };
keyof 연산자를 typeof 연산자와 함께 사용하면 객체의 키를 추출하여 객체의 프로퍼티에 안전하게 접근할 수 있다.
type Point = { x: number; y: number };
const pos: Point = {
x: 255,
y: 255,
};
// axios: "x" | "y"
function getXPosition(pos: Point, axis: keyof typeof Point) {
return pos[axis];
}
keyof 타입은 맵드 타입과 함께 사용할 때 유용하다.
맵드 타입
맵드 타입은 기존의 객체 타입을 기반으로 새로운 객체 타입을 만드는 타입 조작 기능이다.
interface User {
id: number;
name: string;
age: number;
}
function updateUser(user: User) { ... }
updateUser({ // ❌
age: 25
});
updateUser 함수는 User 타입인 객체를 전달 받아서 유저 정보를 전달하는 함수이다. 유저 정보를 수정하기 위해 변경하고 싶은 부분인 age 프로퍼티만 전달하려고 한다. 하지만 함수 매개변수 타입이 User 타입으로 되어 있어 불가능하다.
새로운 타입을 정의해보자.
interface User {
id: number;
name: string;
age: number;
}
interface PartialUser {
id?: number;
name?: string;
age?: number;
}
function updateUser(user: PartialUser) {}
updateUser({ // ✅
age: 25
});
원하는 방식으로 동작하지만 User와 PartialUser는 중복된 프로퍼티를 정의하고 있다. 이럴 때 맵드 타입을 이용한다. PartialUser 타입을 맵드 타입을 이용해 정의하면 아래와 같다.
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = {
[key in "id" | "name" | "age"]?: User[key];
};
[key in "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];
};
템플릿 리터럴 타입
템플릿 리터럴 타입은 템플릿 리터럴을 이용해 특정 패턴을 갖는 String 타입을 만드는 기능이다. 이벤트와 이벤트 핸들러의 타입을 정의하는 예시를 들어 살펴보자.
type EventName = "click" | "change" | "focus";
type EventHandlers = {
onClick: () => void;
onChange: () => void;
onFocus: () => void;
};
이때 이벤트가 하나가 추가될 때마다 각 타입에 새롭게 정의 해주어야 한다. 이벤트의 개수가 늘어날수록 유지보수가 어렵고 실수할 가능성이 높아진다. 템플릿 리터럴 타입을 사용하여 이 문제를 해결할 수 있다.
type EventName = "click" | "change" | "focus" | "blur";
type EventHandlers = {
[key in `on${Capitalize<EventName>}`]: () => void;
};
Capitalize<String>
: 문자열의 첫 번째 문자를 해당하는 대문자로 변환
댓글남기기