티스토리 뷰
타입스크립트 | 타입 조작 - 인덱스드 엑세스 타입, keyof 연산자, mapped 타입, 템플릿 리터럴 타입, 조건부 타입
Nyugati 2023. 8. 27. 00:41타입 조작하기
타입 조작이란, 여러 타입들을 상황에 따라 다른 타입으로 변환하는 기능을 말한다.
타입 조작 예시
- 제네릭
- 인덱스드 엑세스 타입
- keyof 연산자
- mapped 타입
- 템플릿 리터럴 타입
- 조건부 타입
1. 인덱스드 엑세스 타입
객체, 배열, 튜플 등 복잡하고 큰 타입에서 특정 프로퍼티 혹은 요소의 타입만 추출하는 타입을 말한다.
(1) 객체 프로퍼티 타입 추출하기
아래는 Post 객체에서 author 프로퍼티 타입을 추출하는 과정이다.
interface Post {
title: string;
content: string;
author: {
id: number;
name: string;
};
}
// 인덱스드 엑세스 타입
function printAuthorInfo(author: Post["author"]) {
console.log(`${author.id} - ${author.name}`);
}
// 중첩으로 가져오기
function printAuthorId(author: Post["author"]["id"]) {
console.log(author);
}
const post1: Post = {
title: "제목",
content: "본문",
author: {
id: 1,
name: "홍길동",
},
};
Post["author"]
를 활용해서 Post 타입으로부터 author 프로퍼티의 타입을 추출한다. 참고로 인덱스에는 값이 아니라 타입만 들어갈 수 있다.
(2) 배열 요소의 타입 추출하기
특정 배열의 요소 타입을 추출할 수도 있다. 아래는 Post 타입 배열에서 요소의 타입만 뽑아오는 코드이다. 이때 마찬가지로 인덱스드 엑세스 타입을 이용해 PostList[number]
로 배열에서 요소를 하나 뽑아오고 그 요소 안의 author 프로퍼티에 접근한다.
type PostList = {
title: string;
content: string;
author: {
id: number;
name: string;
};
}[];
function printAuthorInfo2(author: PostList[number]["author"]) {
console.log(`${author.id} - ${author.name}`);
}
참고로 인덱스에 Number literal 타입을 넣을 수도 있다.
const post2: PostList[0] = {
title: "제목",
content: "본문",
author: {
id: 1,
name: "홍길동",
},
};
(3) 튜플의 요소 타입 추출하기
튜플의 각 요소도 인덱스드 엑세스 타입으로 추출할 수 있다.
type Tup = [number, string, boolean];
type Tup0 = Tup[0];
type Tup1 = Tup[1];
type Tup2 = Tup[2];
넘버 리터럴이 아닌 number 타입으로 인덱스에 접근하면 튜플 타입안에 있는 모든 타입의 최적의 공통타입을 가져온다. 아래의 예시에서는 union 타입으로 가져온다.
type TupNum = Tup[number];
2. keyof 연산자
특정 객체 타입으로부터 프로퍼티 key들을 모두 스트링 리터럴 유니온 타입으로 추출하는 연산자를 말한다. keyof
연산자는 반드시 타입과 함께 사용해야 한다(변수와 사용 안됨).
아래는 keyof 연산자의 활용 예시이다. keyof 타입
형태로 사용되며 타입
의 모든 프로퍼티 key를 string literal union 타입으로 추출한다.
interface Person {
name: string;
age: number;
}
function getPropertyKey(person: Person, key: keyof Person) {
return person[key];
}
const person: Person = {
name: "홍길동",
age: 60,
};
getPropertyKey(person, "name");
typeof 연산자와의 활용
typeof
연산자를 사용하여 타입을 정의하면 특정 변수의 타입을 추론할 수 있다.
type Person2 = typeof person;
function getPropertyKey2(person: Person, key: keyof typeof person) {
return person[key];
}
3. 맵드 타입
기존의 객체 타입으로부터 새로운 객체 타입을 만드는 타입을 말한다.
참고로 맵드 타입은 인터페이스에서는 사용할 수 없으므로 타입 별칭과 함께 사용해야 한다.
아래는 맵드 타입의 사용 예시이다. 유저 정보를 관리하는 프로그램으로 유저의 정보는 서버에 저장되어있다고 가정하자.
interface User {
name: string;
id: number;
age: number;
}
// 한명의 유저 정보 불러오기
function fetchUser(): User {
return {
name: "홍길동",
id: 1,
age: 30,
};
}
// 한명의 유저 정보 수정하기
function updateUser(user: PartialUser) {
// 수정하는 기능
}
User 인터페이스로부터 파생된 여러 타입을 만들어보자.
type PartialUser = {
// User 인터페이스 객체의 모든 프로퍼티를 선택적 프로퍼티로 만들기
[key in "id" | "name" | "age"]?: User[key];
};
type booleanUser = {
// User 인터페이스 객체의 모든 프로퍼티를 boolean 타입으로 만들기
// [key in "id" | "name" | "age"]: boolean;
[key in keyof User]: boolean; // keyof 연산자 활용가능
};
type readonlyUser = {
// 읽기 전용 유저정보
readonly [key in keyof User]: User[key];
};
key in "id" | "name" | "age"
은 객체 타입이 갖는 키들을 의미하며 :
뒤에 오는 값은 key에 해당하는 value를 의미한다. keyof
연산자를 활용하여 특정 객체 안의 있는 key들을 쉽게 불러올 수도 있다.
4. 템플릿 리터럴 타입
스트링 리터럴 타입을 기반으로 정해진 패턴의 문자열만 포함하는 타입을 말한다. 아래의 예시를 참고하면 쉽게 이해할 수 있다.
type Color = "red" | "black" | "green";
type Animal = "dog" | "cat";
// Color 타입과 Animal 타입 조합하기
type ColorAnimal = `${Color}-${Animal}`;
const myColorAnimal: ColorAnimal = "black-cat";
ColorAnimal 타입은, 3개의 타입으로 구성된 Color union 타입과 2개의 타입으로 구성된 Animal union 타입을 조합하여 만들 수 있는 모든 가지수의 String literal 타입이 된다.
5. 조건부 타입
조건부 타입은 extends
와 삼항연산자
를 활용하여 조건에 따른 타입을 결정하는 문법을 말한다.
type A = number extends string ? string : number;
number extends string ?
라는 조건식에서 참이라면 ? 우측의 타입이 되고, 거짓이라면 : 우측의 타입이 결과가 된다.
객체 타입에서도 쉽게 사용할 수 있다.
type ObjA = {
a: number;
};
type ObjB = {
a: number;
b: number;
};
type B = ObjB extends ObjA ? number : string; // number type
ObjB는 ObjA의 서브 타입이 되므로 조건식이 참이되고 결과는 number 타입이 된다.
(1) 제네릭 조건부 타입
조건부 타입은 제네릭과 함께 사용할 때 위력이 극대화 된다.
아래의 예시는 타입 변수에 number 타입이 할당되면 string 타입을 반환하고 그렇지 않으면 number 타입을 반환하는 조건부 타입이다.
type StringNumberSwitch<T> = T extends number ? string : number;
let temp1: StringNumberSwitch<number>; // string 타입
let temp2: StringNumberSwitch<string>; // number 타입
아래의 예시는 공백을 제거하는 함수로, 타입 변수가 string 타입이면 공백이 제거된 string을 반환하고 다른 타입이면 undefined를 반환한다.
이때 발생할 수 있는 에러를 해결하기 위해선 함수 오버로딩
을 이용하여 오버로드 시그니쳐에 조건부 타입을 명시해줄 수 있다. 이때 반환값의 타입을 T extends string ? string : undefined
와 같이 지정한다.
// 오버로딩 활용
function removeSpaces<T>(text: T): T extends string ? string : undefined;
function removeSpaces(text: any) {
if (typeof text === "string") {
return text.replaceAll(" ", "");
} else {
return undefined;
}
}
let result = removeSpaces("hello typescript!"); // string으로 추론
let result2 = removeSpaces(undefined); // undefined로 추론
(2) 분산적인 조건부 타입
타입 변수에 유니온 타입을 할당하면 union 타입의 모든 멤버 타입들이 분리된다.
type StringNumberSwitch<T> = T extends number ? string : number;
let d: StringNumberSwitch<boolean | string | number>;
즉, 위의 예시에서는 변수 d는 한 번은 boolean, 한 번은 string, 한 번은 number로 분리되어 타입변수에 할당되고, 이후 분리된 결과를 각각 union으로 묶어주는 것이다. 최종적으로 변수 d는 number|string
타입으로 정의된다.
이를 단계별로 나타내면 아래와 같다.
- 멤버 타입 분리
- StringNumberSwitch<boolean>
- StringNumberSwitch<string>
- StringNumberSwitch<number>
- 각 타입별 타입 추론
- number
- number
- string
- union
- 결과 : number | string
아래는 분산적인 조건부 타입의 또 다른 예시들이다.
// 예제 1 : 특정 타입만 제외하기
// T 타입이 U 타입의 서브타입이 되면 never 타입으로, 아니라면 그대로
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string | boolean, string>;
// 1 단계
// Exclude<number, string>
// Exclude<string, string>
// Exclude<boolean, string>
// 2 단계
// number
// never
// boolean
// 결과 : 공집합인 never와 합집합하면 never 는 사라짐
// number | never | boolean => number | boolean
// 예제 2 : 특정 타입만 추출하기
type Extract<T, U> = T extends U ? T : never;
type B = Extract<number | string | boolean, string>;
// 1 단계
// Extract<number, string>
// Extract<string, string>
// Extract<boolean, string>
// 2 단계
// never
// string
// never
// 결과 : 공집합인 never와 합집합하면 never 는 사라짐
// never | string | never => string
(3) infer : 타입 추론하기
infer
는 조건부 타입 내에서 특정 타입만 추론해 올 수 있는 기능을 말한다. infer는 다음과 같이 특정 함수 타입에서 반환값의 타입만 추출하는 특수한 조건부 타입인 ReturnType을 만들 때 이용할 수 있다.
type ReturnType<T> = T extends () => infer R ? R : never;
조건식 T extends () => infer R
에서 infer R
은 이 조건식이 참이 되도록 만들 수 있는 최적의 R 타입을 추론하라는 의미이다. 따라서 아래와 같이 타입이 추론된다.
type FuncA = () => string;
type FuncB = () => number;
type A = ReturnType<FuncA>; // string 으로 추론
type B = ReturnType<FuncB>; // number로 추론
type C = ReturnType<number>; // 추론 불가의 상황 => never 타입 반환
아래의 예제는 프로미스의 resolve 반환 객체 타입을 조사하는 과정이다. 이때 PromiseUnpack 타입에서 타입 변수는 프로미스여야 하며, 프로미스 타입의 결과값 타입을 반환해야 한다.
type PromiseUnpack<T> = T extends Promise<infer R> ? R : never;
type PromiseA = PromiseUnpack<Promise<number>>; // number로 추론
type PromiseB = PromiseUnpack<Promise<string>>; // string 으로 추론
본 포스팅은 인프런 한 입 크기로 잘라먹는 타입스크립트 강의를 기반으로 작성되었습니다. 사용된 이미지들은 모두 위 강의에서 가져왔습니다. 문제시 알려주세요.
'웹개발 > TypeScript' 카테고리의 다른 글
타입스크립트 | 유틸리티 타입 (0) | 2023.08.28 |
---|---|
타입스크립트 | 제네릭 (0) | 2023.08.26 |
타입스크립트 | 클래스 (0) | 2023.08.24 |
타입스크립트 | 인터페이스 (0) | 2023.08.24 |
타입스크립트 | 함수와 타입 (0) | 2023.08.24 |