타입스크립트 심화 (2)
타입스크립트 개념 보충하기 2편
Indexed Access
특정 객체 타입을 정의해 놓고 객체 내 특정 프로퍼티의 타입을 필요로 할 경우 사용한다. 주로 복잡하고 큰 타입으로부터 내부 작은 단위의 타입을 얻어낼 때 사용한다.
필요한 프로퍼티의 타입은 선언된 전체 타입의 key를 string 리터럴로 가져오면 된다. 이 때 key의 type
으로부터 특정 타입을 가져오는 것이기 때문에 대괄호 내부는 변수(값)를 넣을 수 없다.
interface Post {
title: string;
description: string;
author: {
id: number;
name: string;
age: number;
}
};
function getAuthor (author: Post["author"]) {
console.log(author.id, author.name, author.age);
};
indexed access 타입을 활용하면 배열 타입으로부터 하나의 요소 타입을 가져올 수 있다.
예시로 Post 타입을 배열로 갖는 PostList 타입이 있다고 할 때 이 PostList 타입으로부터 하나의 Post를 얻기 위해서 indexed access 타입을 사용할 수도 있다. 특정 index로부터 값을 가져와야 하기 때문에 아래와 같이 대괄호 [] 내부에 number 타입을 넣어준다.
type PostList = {
title: string;
description: string;
author: {
id: number;
name: string;
age: number;
}
}[];
const targetPost: PostList[number] = {
title: '제목',
description: '설명',
author: {
id: 1,
name: 'yun',
age: 20
}
};
튜플을 사용할 경우 각각 인덱스에 대한 타입을 지정할 수 있다.
type TupleType = [string, number, boolean];
type TupStr = TupleType[0];
type TupNum = TupleType[1];
type TupBool = TupleType[2];
type TupErr = TupleType[3]; // ❌ 존재하지 않기 때문에 에러 발생
keyof
객체 프로퍼티의 key를 유니온 타입으로 추출할 때 사용하는 키워드다.
타입스크립트에서 사용하는 기존의 typeof 연산자와도 같이 사용할 수 있는데 특정 변수의 타입을 얻어낼 때 사용된다.
interface Post {
title: string;
description: string;
author: {
id: number;
name: string;
age: number;
}
};
// keyof Post = "title" | "description" | "author"
function getPropertyKey (post: Post, key: keyof Post) {
return post[key];
};
// PostType => { title: string, description: string}
type PostType = typeof post;
const post = {
title: '제목',
description: '설명'
};
keyof, typeof를 한번에 사용하면 위 getPropertyKey를 다른 방식으로 재정의할 수 있다.
interface Post {
title: string;
description: string;
author: {
id: number;
name: string;
age: number;
}
};
function getPropertyKey (post: Post, key: keyof typeof post) {
return post[key];
};
Mapped
기존에 정의된 타입을 사용자 입맛에 맞게 다양하게 변환시킬 때 사용한다. interface 키워드에서는 사용할 수 없고 type 별칭에서만 사용할 수 있다.
interface Post {
title: string;
description: string;
author: {
id: number;
name: string;
age: number;
}
};
// Post 타입의 프로퍼티를 모두 선택적 프로퍼티로 변경
type PartialPost = {
[key in keyof Post]?: Post[key];
}
// Post 타입의 프로퍼티 타입을 모두 boolean으로 변경
type BooleanPost = {
[key in keyof Post]: boolean;
};
// Post 타입의 프로퍼티를 모두 readonly로 변경
type ReadonlyPost = {
readonly [key in keyof Post]: Post[key];
};
템플릿 리터럴
템플릿 리터럴을 조합하여 새로운 타입을 만들 때 사용한다.
type Animal = 'dog' | 'cat';
type Color = 'red' | 'yellow';
// red-dog | red-cat | yellow-dog | yellod-cat
type ColoredAnimal = `${Color}-${Animal}`;
단일 조건부 타입
조건에 따라 타입을 결정하는 방법으로 자바스크립트의 삼항 연산자를 사용한다.
type A = {
a: number
};
type B = {
a: number;
b: number;
};
// 타입 A가 타입 B의 슈퍼 타입일 때 타입 C는 number, 슈퍼 타입이 아닐 때 타입 C는 string
type C = B extends A ? number : string; // number
제네릭과 사용하면 활용도가 높아진다. 아래 코드는 타입 T가 string일 경우 StringNumberSwitch<T> 타입은 number, 타입 T가 number일 경우 StringNumberSwitch<T> 타입은 string이 되도록 하고 있다.
type StringNumberSwitch<T> = T extends string ? number : string;
let a: StringNumberSwitch<number>; // string 타입
let b: StringNumberSwitch<string>; // number 타입
아래 예제에서는 인수의 타입에 따라 함수의 결과 타입을 다르게 얻는다. 다만 함수 내부에서는 제네릭 T가 unknown 타입이기 때문에 타입 가드로 타입을 좁히기 어렵다.('특정 타입 <- unknown' 불가능 하기 때문) 따라서 이 경우에는 리턴값에 any 타입 단언을 사용하거나 함수 오버로딩을 사용하여 타입을 재정의 해야한다.
any를 사용하면 값 자체의 타입이 any가 되어버리기 때문에 string, undefined 외 다른 타입이 return에 사용될 수 있어 함수 자체에서 타입 에러가 발생하지 않는다. 함수 오버로딩을 사용하면 오버로드 시그니처에서 타입이 다를 경우 타입 에러를 발생시키기 때문에 이 경우 any 보다 함수 오버로딩을 사용하는 것이 좋다.
// 1) any 사용
function removeSpaces<T>(text: T): T extends string ? : string : undefined {
if (typeof text === 'string') {
return text.replaceAll(" ", "") as any;
} else {
return undefined as any;
}
}
// 2) 함수 오버로딩 사용
function removeSpaces<T>(text: T): T extends string ? : string : undefined
function removeSpaces (text: any) {
if (typeof text === 'string') {
return text.replaceAll(" ", "");
} else {
return undefined;
}
}
분산적 조건부 타입
제네릭 T에 단일 타입이 아닌 유니온 타입이 할당될 경우 삼항 연산자는 위와 다르게 작동한다. 최종 타입은 각각의 유니온 타입의 결과들이 하나의 유니온으로 묶이게 된다.
type StringNumberSwitch<T> = T extends string ? number : string;
let a: StringNumberSwitch<number>; // string 타입
let b: StringNumberSwitch<string>; // number 타입
let c: StringNumberSwitch<string | number | boolean>;
// 각각의 유니온 타입의 결과들
// StringNumberSwitch<string> : number
// StringNumberSwitch<number> : string
// StringNumberSwitch<boolean> : string
// 하나의 유니온으로 묶인다.
// number | string
이를 활용하여 유니온 타입에서 특정 타입만 추출 또는 제거하여 사용할 수 있다.
타입 추출
type Extract<T, U> = T extends U ? T : never;
type A = Extract<string | number | boolean, string>;
// 각각의 유니온 타입의 결과들
// Extract<string, string> : string
// Extract<number, string> : never
// Extract<boolean, string> : never
// 하나의 유니온으로 묶인다.
// string | never
// 결과에 never가 포함되면 제거 => 공집합이기 때문에
// string
타입 제거
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<string | number | boolean, string>;
// 각각의 유니온 타입의 결과들
// Exclude<string, string> : never
// Exclude<number, string> : number
// Exclude<boolean, string> : boolean
// 하나의 유니온으로 묶인다.
// never | number | boolean
// 결과에 never가 포함되면 never 제거
// number | boolean
분산적 조건부 방지
각각의 유니온 타입의 결과를 얻는 것이 아닌, T 자체에 유니온을 넣고 싶을 때는 타입에 대괄호를 씌워준다.
type StringNumberSwitch<T> = [T] extends [string] ? number : string;
let a: StringNumberSwitch<string | number | boolean>; // string
위 예제에서는 [string | number | boolean]은 string의 서브 타입이 아니기 때문에 결과 타입은 항상 string이 된다.
infer
명확하게 정해진 타입이 아닌 특정 타입을 추론하여 얻어낼 때 사용한다.
type ReturnType<T> = T extends () => infer R : R : never;
let a: ReturnType<() => number>; // number
let b: ReturnType<() => string>; // string
let c: ReturnType<number>; // never