2.1Advanced Types
타입스크립트 심화 개념들-Type Guard, Discriminated Unions, instanceof, index types, as const, satisfies
추억의 쪽지 시험
Intersection Types
타입들을 &
로 여러개 합친, 교집합 타입을 만들 수 있다.
typescript
type FileData = {path: string;content: string;};type Status = {isOpen: boolean;errorMessage?: string;}type AccessedFileData = FileData & Status;
이 방식은 이전 포스팅에서 살펴본 interface
에서 extends
를 활용한 문법과 거의 비슷한 효과다.
typescriptinterface FileData {}; interface Status {}; interface AccessedFiledData extends FileData, Status;
A & B
는 충돌 속성의 타입이 교집합으로 좁혀져 never
가 된다. 즉, 구현 불가 타입이 된다. 반면, interface C extends A, B {}
는 컴파일 오류로 거부하기 때문에 보다 즉각적으로 오류를 알아차릴 수 있게 된다.Type Guard
함수에 전달될 수 있는 데이터가 여러 종류의 타입을 다 받을 수 있다면, 함수 안의 로직은 적절한 분기를 통해 각 타입에 대한 코드를 따로 작성해야 한다.
아래는 if
조건문을 활용해 해당 타입 내부의 속성을 점검하는 방식이다.
typescript
type FileSource = { path: string };type DBSource = { connectionUrl: string };type Source = FileSource | DBSource;function loadData(source: Source){if('path' in source){// source.path;return;}// source.connectionUrl;}
Discriminated Unions
위 방식보다 더 자주 쓰이는 패턴은 타입을 구분할 수 있는 구분자 속성을 타입에 포함하는 것이다. 예를 들어, 기존엔 경로만 포함했던 위의 두 다른 Source
타입들에 대해 type
이라는 속성을 추가하고, 이 속성에 해당 타입이 어떤 종류의 source인지를 표시하는 것이다. 이러면 똑같이 조건문을 쓰더라도 분기시 의미가 있는 단어를 통해 분기하므로 코드의 가독성이 더 좋아진다.
이런 패턴을 Discriminated Unions라고 부른다.
typescript
type FileSource = { type: 'file', path: string };type DBSource = { type:'db',connectionUrl: string };type Source = FileSource | DBSource;function loadData(source: Source){if(source.type === 'file'){// source.path;return;}// source.connectionUrl;}
보다 안전하게 하기 위해선 switch
문으로 각기 case
들을 나눠 코딩할 수 있다. 또, default
케이스를 추가해 새로운 케이스가 들어올 시 오류로 잡을 수 있게 한다.
typescript
function loadData(source: Source){switch(source.type){case 'file':return source.path;case 'db':return source.connectionUrl;default: {const _exhaustive: never = source; // 새 variant 추가 시 오류로 잡아줌return _exhaustive;}}}
Discriminated unions 방식은 '아웃소싱'이 가능하다. 즉, 타입을 체크하는 별도의 함수를 밖에서 정의한 후 활용하는 것이다.
typescriptfunction isFile(source: Source){ return source.type === 'file'; }
instanceof
instanceof
는 확인하고자 하는 변수가 특정 클래스의 인스턴스인지 확인하는 키워드이다. 런타임에 실제 클래스(생성자)가 있어야만 동작하며, interface
나 type
에는 쓸 수 없다.
typescriptclass User { constructor(public name: string){} join(){ // .... } }; class Admin{ scan(){} } type Entity = User | Admin; function init(entity: Entity){ if(entity instanceof User){ entity.join(); return; } entity.scan(); }
Function Overloads
만약 문자열 또는 배열을 받았을 때, 문자열이면 문자열의 단어 개수, 배열이면 배열의 요소 수를 반환하는 함수를 작성한다고 해보자. 앞서 배운 것처럼 아래와 같은 type guard를 해볼 수 있다.
typescriptfunction getLength(val: string | any[]){ if(typeof val === 'string'){ const numberOfWords = val.split(' ').length; return `${numberOfWords} words`; } return val.length; }
그런데, 이 방식으로는 문자열은 문자열을 반환, 배열은 숫자를 반환하기에 결과값을 활용하려면 또 분기가 필요해진다.
따라서 이 경우, 아래와 같은 function overload(함수 오버로딩)라고 불리는 기법을 통해 매개변수 별로 다른 타입을 반환하는 함수의 여러 버전을 정의할 수 있다. 함수 오버로딩은 시그니처라고 불리는 여러 개의 타입 지정과 구현부라고 불리는 1개의 실제 구현 코드로 이루어진다.
typescript
function getLength(val: any[]): number;function getLength(val: string): string;function getLength(val: string | unknown[]){if(typeof val === 'string'){const numberOfWords = val.split(' ').length;return `${numberOfWords} words`;}return val.length;}
Index Types
인덱스 타입이란, 해당 타입이 가질 수 있는 속성의 키의 타입과 값의 타입을 정의하는 것이다.
예를 들어, 아래의 DataStore 타입은 문자열로된 키와 숫자 또는 불리언이 값인 타입이다. 이렇게 타입을 지정하면 키와 값의 타입은 제한하면서도 유연한 객체를 만들 수 있다.
typescripttype DataStore = { [prop: string]: number | boolean; } let store: DataStore = {}; store.id = 5; store.isOpen = false;
참고로, 이런 케이스는 Record
타입으로도 동일하게 정의할 수 있다.
typescriptlet store: Record<string, number | boolean> = {};
as const
as const
는 타입스크립트에 타입 추론을 할 때 최대한 세부적으로 하라는 지시어다. 예를 들어, 문자열들의 배열을 변수에 할당하면 해당 변수의 타입은 자동으로 string[]
이 된다. 하지만, 아래와 같이 as const
키워드를 붙이면 배열에 있는 항목들로만 타입을 제한한다. 이렇게 만든 변수는 **읽기 전용 튜플**이 된다.
typescriptconst roles = ['admin', 'guest', 'editor'] as const; // roles: readonly ['admin', 'guest', 'editor'] (읽기 전용 튜플) // 읽기 전용 배열을 파라미터로 받으려면 function allow(r: readonly string[]) { /* ... */ } allow(roles); // OK
여기서 roles
의 요소들로 이루어진 Role이라는 타입을 추출할 때, roles
의 인덱스에 접근하라는 식으로 유니언 타입을 추출할 수 있는데, 이를 유니언 타입 추출이라고 하고 as const
의 주요 활용처 중 하나이다.
typescriptconst roles = ['admin', 'guest', 'editor'] as const; type Role = typeof roles[number]; // 'admin' | 'guest' | 'editor'
satisfies
아래와 같은 dataEntries 레코드가 있다고 하자.
typescriptconst dataEntries: Record<string, number> = { entry1: 0.51, entry2: -1.23 }
언뜻 타입을 잘 제한하고 entry들을 잘 저장한것 같지만, 이렇게 레코드를 만들 경우 없는 속성도 <문자열, 숫자>
조합으로 접근 가능해진다.
typescriptdataEntries.entry3; // ??
만약 타입 지정(Record<string, number>
)를 지운다면 타입스크립트가 객체를 추론하지만, 이 경우 내부 데이터에 대한 타입 제한이 되지 않는다. 이럴때 satisfies
키워드를 아래와 같이 활용한다.
typescript
const dataEntries = {entry1: 0.51,entry2: -1.23} satisfies Record<string, number>;dataEntries.entry3; // 존재하지 않는 속성