이 글은 아래 강의를 바탕으로 정리한 글입니다. 🤗
https://inf.run/UGoRu타입을 조작한다는 것은 기본 타입이나 타입 별칭 또는 인터페이스로 만든 원래 존재하던 타입들을 상황에 따라 유동적으로 다른 타입으로 변환하는 TS의 강력한 기능입니다.
📌 타입 조작 기능은 아래와 같이 6가지로 나눌 수 있습니다.
제네릭
인덱스드 엑세스 타입
keyof 연산자
Mapped 타입
템플릿 리터럴 타입
조건부 타입
인덱스드 엑세스 타입은 인덱스를 이용해 객체, 배열, 튜플 타입으로부터 특정 프로퍼티의 타입을 추출하는 타입입니다.
아래와 같이 게시글을 표현하는 객체 타입이 있다고 가정하고 게시글 post를 하나 생성합니다.
그리고 이 게시글에서 작성자의 이름과 아이디를 붙여서 추출하는 함수도 정의해주었을 때,
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
};
}
const post: Post = {
title: "Hello World",
content: "This is a test post",
author: {
id: 1,
name: "John"
}
}
function printAuthorInfo(author: {id: number, name: string}){
console.log(`${
author.id
} - ${
author.name
}`)
}⚠️ 만약 Post 타입의 author 프로퍼티 타입에 age 라는 프로퍼티가 추가되거나 id, name과 같은 프로퍼티 이름이 수정된다면 printAuthorInfo 함수의 매개변수 타입도 그때마다 계속 수정해줘야하는 불편함이 존재합니다.
이런 경우 다음과 같이 인덱스드 엑세스 타입을 이용하여 Post 타입에서 author 프로퍼티의 타입을 추출해 사용하면 편리합니다.
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 type으로부터 author 프로퍼티의 타입을 추출합니다.
그 결과 author 매개변수의 타입은 {id: number, name: string, age: number}가 됩니다.
이때, 대괄호에 들어가는 String Literal 형태의 "author"를 인덱스라고 부르며, 이러한 인덱스를 이용해 특정 타입에 접근한다고 하여 인덱스드 엑세스 타입이라고 부릅니다.
🚨 주의할 점은
인덱스에는 값이 아니라 타입만 들어갈 수 있습니다!
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,
},
};이전 Post 타입을 배열로 수정한 뒤, 인덱스드 엑세스 타입을 이용해 위와 같이 PostList 배열 타입에서 하나의 요소의 타입만 추출하는 코드입니다.
PostList[number]는 PostList 배열 타입으로부터 요소의 타입을 추출하는 인덱스드 엑세스 타입입니다.
이렇듯 배열의 요소 타입을 추출할 때에는 인덱스에 number 타입을 넣어주면 됩니다.
⚠️ 주의할 점은 튜플 타입에 인덱스드 엑세스 타입을 사용할 때, 인덱스에 number 타입을 넣으면 튜플 타입 안에 있는 모든 타입의 최적의 공통 타입을 추출합니다.
// 튜플 타입
type Tup = [number, string, boolean];
type Tup0 = Tup[0]; // number
type Tup1 = Tup[1]; // string
type Tup2 = Tup[2]; // boolean
// type Tup3 = Tup[3]; // 존재하지 않는 인덱스에 접근하면 오류 발생
type TupNumber = Tup[number]; // Tuple 타입 안에 있는 모든 타입의 최적의 공통 타입을 추출 -> string | number | booleankeyof 연산자는 객체 타입으로부터 프로퍼티의 모든 키들을 String Literal Union 타입으로 추출하는 연산자입니다.
즉, 연산자 우측에 있는 객체 타입으로부터 프로퍼티의 모든 key들을 Union 타입으로 추출하는 연산자입니다.
🚨 주의사항으로 keyof 연산자는 무조건! 타입에만 사용할 수 있습니다.
interface Person {
name: string;
age: number;
}
function getPropertyKey(person: Person, key: keyof Person) {
return person[key];
}
const person: Person = {
name: "John",
age: 30,
};Person이라는 객체 타입을 정의하고 해당 타입을 갖는 변수를 하나 선언했습니다.
그리고 getPropertyKey라는 이름의 함수를 만들었는데 이 함수는 2개의 매개변수 중 두번째 매개변수 key에 해당하는 프로퍼티의 값을 첫번째 매개변수에서 꺼내 반환하는 역할을 합니다.
만약 두번째 매개변수의 key 타입을 key: "name" | "age"와 같이 정의했다면 Person 객체에 프로퍼티가 추가되거나 수정될 경우 이 매개변수의 타입도 계속해서 수정해야하는 번거로움이 있을 것입니다.
이런 경우 위와 같이 keyof 연산자를 이용하면 좋습니다. 따라서 위 코드에서 keyof Person의 결과 값은 "name" | "age"로 하드코딩할 때와 동일한 결과를 받을 수 있습니다.
💡 한가지 주의할 점은 Keyof 연산자는 오직 타입에만 적용할 수 있는 연산자라는 점입니다.
typeof 연산자는 JS에서 특정 값의 타입을 문자열로 반환하는 연산자입니다. 그러나 다음과 같이 타입을 정의할 때, 사용하면 특정 변수의 타입을 추론하는 기능도 가지고 있습니다.
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: "Jhon",
age: 27,
};맵드 타입은 기존의 객체 타입을 기반으로 새로운 객체 타입을 만드는 타입 조작 기능 중 하나입니다.
⚠️ 이러한 맵드 타입은 interface에서는 만들 수 없으며, 오직 type alias에서만 사용할 수 있습니다.
예시로 유저 정보를 관리하는 간단한 프로그램의 일부분을 만들기 위해 유저 객체 타입을 정의하고 서버에 저장되어 있는 한명의 유저 정보를 불러오는 기능과 그 정보를 수정하는 기능을 만들었다고 가정하겠습니다.
interface User {
id: number;
name: string;
age: number;
}
function fetchUser(): User {
return {
id: 1,
name: "John",
age: 20
}
}
function updateUser(user: User){
// ... 유저정보 수정 기능
}이때, 유지 정보를 수정하려면 아래와 같이 updateUser 함수를 호출하고 여러 개의 정보 중 수정하고 싶은 프로퍼티만 전달해주면 됩니다.
그러나 updateUser 함수의 매개변수 타입이 User 타입으로 되어 있어 수정하고 싶은 프로퍼티만 골라서 보낼 수 없는 상황입니다.
그래서 다음과 같이 수정하길 원하는 프로퍼티만 전달할 수 있도록 새로운 타입을 만들어주어야 합니다.
interface User {
id: number;
name: string;
age: number;
}
// 추가 타입 생성
interface PartialUser {
id?: number;
name?: string;
age?: number;
}
function fetchUser(): User {
return {
id: 1,
name: "John",
age: 20
}
}
// 매개변수 타입 변경
function updateUser(user: PartialUser){
// ... 유저정보 수정 기능
}
updateUser({ // ❌
age: 25
});그런데 보면 User 타입과 PartialUser 타입이 지금 서로 중복된 프로퍼티를 정의하고 있습니다.
따라서 이런 경우 바로 Mapped Type을 이용합니다.
type PartialUser = {
[key in "id" | "name" | "age"]?: User[key];
// keyof 연산자를 사용하여 업그레이드
// [key in keyof User]?: User[key];
};이는 기존 타입의 모든 프로퍼티를 선택적으로 만들어주는 맵드 타입입니다.
[key in "id" | "name" | "age"]?: User[key]를 해석해보면 다음과 같습니다.
💡 이 객체 타입은 key가 한번은 id, 한번은 name, 한번은 age가 된다는 뜻이며, 따라서 3개의 프로퍼티를 갖는 객체 타입으로 정의됩니다.
key가 "id" 일 때 →
id : User[id]→id : numberkey가 "name"일 때 →
name : User[user]→name : stringkey가 "age"일 때 →
age : User[age]→age : number
즉, key in 다음 User 객체의 모든 프로퍼티의 키들을 입력하고
대괄호 [] 부분은 이 객체의 프로퍼티 키가 무엇이 될 수 있는지 정의하는 부분이며,
: 다음은 프로퍼티의 키들이 어떤 value 타입을 가질지 정의합니다.
그리고 키가 끝나는 지점(대괄호가 끝나는 지점)에 ?를 붙임으로서 맵드 타입이 정의하는 모든 프로퍼티가 일괄적으로 선택적 프로퍼티가 되도록 합니다.
템플릿 리터럴 타입은 String Literal 타입을 기반으로 정해진 패턴의 문자열만 포함하는 타입입니다.
type Color = "red" | "blue" | "green";
type Animal = "dog" | "cat" | "bird";
// Template Literal Type
type ColorAnimal = `${Color}-${Animal}`;
// type ColorAnimal = "red-dog" | "red-cat" | "red-bird" | "blue-dog" | "blue-cat" | "blue-bird" | "green-dog" | "green-cat" | "green-bird"
const colorAnimal: ColorAnimal = "red-dog";
const colorAnimal2: ColorAnimal = "blue-cat";
const colorAnimal3: ColorAnimal = "green-bird";Color와 Animal은 각각 3개의 String Literal 타입으로 이루어진 Union type입니다.
그리고 ColorAnimal은 Color와 Animal을 조합해 만들 수 있는 모든 가짓수의 String Literal 타입으로 이루어진 Union type입니다.
💡 만약 Color나 Animal 타입에 String Literal 타입이 추가되어 경우의 수가 많아질 수록 ColoredAnimal 타입에 추가해야하는 타입이 점점 많아지게 되는데, 이럴 때 바로 템플릿 리터럴 타입을 이용하면 좋습니다.
조건부 타입은 extends와 삼항연산자를 이용해 조건에 따라 각각 다른 타입을 정의하도록 돕는 문법입니다.
// number 타입이 string 타입을 확장하는 타입인지 판단하고
// 참이면 string, 거짓이면 number 타입인 A 타입 정의
type A = number extends string ? string : number; // number예를 들어 위 조건부 타입은 삼항연산자를 활용한 조건부 타입이며, 이와 같은 형태로 조건식에 객체 타입을 사용한다면 아래와 같습니다.
type ObjA = {
a: number;
};
type ObjB = {
a: number;
b: number;
};
type B = ObjB extends ObjA ? true : false; // true💡 이렇게만 보면 대체 조건부 타입을 어디에 쓸 수 있을까 싶지만 일반적으로 조건부 타입은 위와 같은 일반 타입에서 사용되기 보다는 제네릭 타입과 함께 사용되며, 이런 방법이 더욱 효과적이라고 합니다.
예를 들어 타입변수 T가 number 타입이라면 이를 string 타입으로 변환하고 반대로 T가 string 타입이라면 이를 number 타입으로 변환하는 함수를 정의해보겠습니다.
type StringNumberSwitch<T> = T extends number ? string : number;
// T의 타입은 number가 되고 조건에 따라 StringNumberSwitch는 string 타입이 됨
let varA : StringNumberSwitch<number> = "hello";
// T의 타입은 string이 되고 조건에 따라 StringNumberSwitch는 number 타입이 됨
let varB : StringNumberSwitch<string> = 123;또다른 예시로 매개변수로 string 타입의 값을 제공받아 공백을 제거한 다음 반환하는 함수를 정의하면 아래와 같습니다.
// 함수 오버로딩 이용
function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces(text: any) {
return text.replaceAll(" ", "");
}
let result = removeSpaces("Hello World");
result.toUpperCase();분산적인 조건부 타입은 조건부 타입을 유니온과 함께 사용할 때, 조건부 타입이 분산적으로 동작하도록 업그레이드 되는 문법입니다.
type StringNumberSwitch2<T> = T extends number ? string : number;
let a: StringNumberSwitch2<number>; // a는 string 타입
let b: StringNumberSwitch2<string>; // b는 number 타입
// 분산적인 조건부 타입
// T가 number | string 타입이라면, T의 각 요소에 대해 조건부 타입이 적용됨
// StringNumberSwitch2<number> -> string
// StringNumberSwitch2<string> -> number
// 따라서 결과적으로 c는 string | number 타입이 됨
let c: StringNumberSwitch2<number | string>;
let d: StringNumberSwitch2<boolean | number | string>;
// 1단계
// StringNumberSwitch2<boolean> |
// StringNumberSwitch2<number> |
// StringNumberSwitch2<string>
// 2단계
// StringNumberSwitch2<boolean> -> number
// StringNumberSwitch2<number> -> string
// StringNumberSwitch2<string> -> number
// 따라서 결과적으로 d는 number | string 타입이 됨위 코드에서 변수 c의 타입변수 T가 number | string 이라면 StringNumberSwitch 타입의 T에 string, number 가 각각 할당이 되고 타입의 결과를 모아 다시 Union 타입으로 묶습니다.
Union으로 3가지 타입을 정의해도 동작방식은 동일하게 타입 변수에 할당한 Union 타입 내부의 모든 타입이 분리되며, 각 타입의 결과를 모아 다시 Union 타입으로 묶습니다.
💡 좀 더 실용적인 예제를 만들어본다면 분산적인 조건부 타입의 특징을 이용하면 매우 다양한 타입을 정의할 수 있는데, 예를 들어 Union 타입으로부터 특정 타입만 제거하는 Exclude 타입을 다음과 같이 정의할 수 있습니다.
type Exclude1<T, U> = T extends U ? never : T;
type AA = Exclude1<number | string | boolean, string>;
// 1단계
// Exclude1<number, string> |
// Exclude1<string, string> |
// Exclude1<boolean, string>
// 2단계
// number |
// never |
// boolean
// 따라서 결과적으로 A는 number | never | boolean 타입이 되나
// 유니온 타입에 never 타입(공집합)이 있으면 제거되므로 결과적으로 AA는 number | boolean 타입이 됨infer란 inferrence(추론)을 의미하며, 조건부 타입 내에서 특정 타입을 추론하는 문법입니다.
inter를 활용하면 다음과 같이 특정 함수 타입에서 반환값의 타입만 추출하는 특수한 조건부 타입인 ReturnType을 만들 때 이용할 수 있습니다.
type ReturnType<T> = T extends () => infer R ? R : never;
type FuncA = () => string;
type FuncB = () => number;
type A = ReturnType<FuncA>;
// string
type B = ReturnType<FuncB>;
// number
type C = ReturnType<number>;
// 조건식을 만족하는 R추론 불가능
// never조건식 T extends () => infer R에서 infer R은 이 조건식을 참이 되도록 만들 수 있는 최적의 R 타입을 추론하라는 의미입니다. (T가 () => R의 서브타입인지를 비교)
📌 따라서 A 타입을 계산할 때의 위 코드의 흐름은 다음과 같습니다.
타입 변수 T에 함수 타입 FuncA가 할당됩니다.
T는 () ⇒ string 이 됩니다.
조건부 타입의 조건식은 다음 형태가 됩니다
() ⇒ string extends () ⇒ infer R ? R : never조건식을 참으로 만드는 R 타입을 추론 합니다 그 결과 R은 string이 됩니다.
추론이 가능하면 이 조건식을 참으로 판단합니다 따라서 결과는 string이 됩니다.