<타입스크립트 프로그래밍> 6장: 고급 타입 - 정리
6장: 고급 타입
6-1. 타입 간의 관계
6-1-1. 서브타입과 슈퍼타입
서브타입은 슈퍼타입을 상속받는 타입이다
B가 A의 서브타입이면 A가 필요한 곳에는 어디든 B를 안전하게 사용할 수 있다
💡 슈퍼타입과 서브타입 예시 1. 배열은 객체의 서브타입이다 2. 튜플은 배열의 서브타입이다 3. 모든 것은 any의 서브터입이다 4. never는 모든 것의 서브타입이다 5. Animal을 상속받는 Bird 클래스가 있다면 Bird는 Animal의 서브타입이다
6-1-2. 가변성
- 보통 A가 B의 서브타입인지 아닌지 쉽게 판단할 수 있다. 특히 number, string 처럼 단순 타입은 쉽게 추론 가능하다. (예: number는 number | string 유니온에 포함되므로 number | string의 서브타입임)
- 매개변수화된(제네릭) 타입 등 복합 타입에서는 이 문제가 더 복잡해진다
형태와 배열 가변성
가변성의 4 종류는 다음과 같다
- 불변 (Invariance) : 정확히 T를 원함
- 공변 (Covariance) : T와 같거나 T의 서브타입을 원함 (<:)
- 반변 (Contravariance) : T와 같거나 T의 슈퍼타입 (>:)
- 양변 (Bivariance) : T와 같거나, T의 슈퍼타입 또나 서브타입을 원함 (<:>)
타입스크립트에서 모든 복합 타입의 맴버(객체, 클래스, 배열, 함수, 반환 타입)는 공변이며, 함수 매개변수 타입만 예외적으로 반변이다
함수 가변성
함수 A가 함수 B와 같거나 적은 수의 매개변수를 가지며, 다음을 만족하면 A는 B의 서브타입이다
- A의 this 타입을 따로 지정하지 않으면 'A의 this 타입 >: B의 this 타입'이다
- A의 각 매개변수 >: B의 대응 매개변수이다
- A의 반환 타입 <: B의 반환타입이다
아래 예제에서는 Crow는 Bird의 서브타입, Bird는 Animal의 서브타입이다.
(Crow <: Bird <: Animal)
class Animal { /* ... */ } class Bird extends Animal { chirp() { /* ... */ } } class Crow extends Bird { caw() { /* ... */ } }
function chirp(bird: Bird): Bird { bird.chirp(); return bird; } chirp(new Bird); // OK chirp(new Crow); // OK chrip(new Animal); // Error: Animal 타입을 매개변수 Bird 타입에 할당할 수 없음
6-1-3. 할당성
'할당성'이란 A라는 타입을 다른 B라는 타입이 필요한 곳에 사용할 수 있는지를 결정하는 타입스크립트의 규칙을 의미한다
'A를 B에 할당할 수 있는가?'라는 질문이 발생하면, 타입스크립트는 다음과 같은 몇 가지 규칙에 따라 처리한다. 배열, 불(boolean), 숫자, 객체, 함수, 클래스, 클래스 인스턴스, 문자열, 리터럴 타입 등 열거형이 아닌 타입에서는 다음의 규칙으로 A를 B에 할당할 수 있는지를 결정한다
1. A <: B 2. A는 any
규칙 1은 서브타입이 무엇인지 정의할 뿐이다. A가 B의 서브타입이면 B가 필요한 곳에는 A를 사용할 수 있다
규칙 2는 예외를 설명하며, 자바스크립트 코드와 상호 운용할 때 유용하다
enum이나 const enum 키워드로 만드는 열거형 타입에서는 다음 조건 중 하나를 만족해야 A 타입을 열거형 B에 할당할 수 있다
1. A는 열거형 B의 맴버다 2. B는 number 타입의 맴버를 최소 한 개 이상 가지고 있으며 A는 number이다
6-1-4. 타입 넓히기
let
,var
같은 키워드를 사용해 값을 바꿀 수 있는 변수를 선언하면, 그 변수의 타입은 리터럴 값에서 값이 속한 기본 타입으로 넓혀진다값을 바꿀 수 없는 변수에서는 리터럴 타입으로 좁혀진다
let a = 'x'; // --> string let b = 3; // --> number const a = 'x'; // --> 'x' const c = true; // --> true
타입을 명시하면 타입이 넓어지지 않도록 막을 수 있다. 하지만 let, var 키워드로 선언했고 타입이 넓혀지지 않은 변수에 값을 다시 하당하면 타입스크립트는 새로운 값에 맞게 변수의 타입을 넓힌다
let a: 'x' = 'x'; // --> 'x' let b: 3 = 3; // --> 3 const a = 'x'; // 'x' let b = a; // string const c: 'x' = 'x'; // 'x' let d = c; // 'x'
null이나 undefined로 초기화된 변수는 any 타입으로 넓혀진다. 하지만 해당 변수가 선언 범위스코프를 벗어나면 타입스크립트는 확실한(좁은) 타입을 할당한다
let a = null; // any a = 3; // any a = 'b'; // any function x() { let a = null; // any a = 3; // any a = 'b'; // any return a; } x(); // string
const 타입
타입스크립트는 타입이 넓혀지지 않도록 해주는
const
라는 특별 타입을 제공한다const를 사용하면 타입 넓히기가 중지되며, 맴버들까지 자동으로
readonly
가 된다 (중첩 자료구조에도 재귀적으로 적용됨)let a = { x: 3 }; // { x: number } let b: { x: 3 }; // { x: 3 } let c = { x: 3 } as const; // { readonly x: 3 } let d = [1, { x: 2 }]; // (number | { x: number })[] let e = [1, { x: 2 }] as const; // readonly [1, { readonly x: 2 }]
초과 프로퍼티 확인
타입스크립트는
신선한 객체 리터럴 타입
의 T를 타들 타입 U에 할당하려는 상황에서 T가 U에 존재하지 않느 프로퍼티를 가지고 있다면, 에러로 처리한다💡 신선한(Fresh) 객체 리터럴 타입 - 타입스크립트가 객체 리터럴로부터 추론한 타입을 가리킴 - 객체 리터럴이 타입 어서션을 사용하거나 변수로 할당되면 '신선함'은 사라진다
type Options = { baseURL: string cacheSize?: number tier?: 'prod' | 'dev' } class API { constructor(private options: Options) { /* ... */ } } /** * Case 1: * 정상 */ new API({ baseURL: 'https://api.mysite.com', tier: 'prod' }) /** * Case 2: * 'badTier'가 아니라 'tier' * 추론된 타입 + 변수할당되지 않았기에 new API에 전달된 객체는 신선한 상태이므로 타입스크립트가 에러를 잡는다 */ new API({ baseURL: 'https://api.mysite.com', badTier: 'prod' }) /** * Case 3: * 'badTier'가 아니라 'tier' * as Options라고 타입 어서션을 했기 때문에 더 이상 new API에 전달된 객체는 신선하지 않다 * 타입스크립트가 에러를 잡지 못한다 */ new API({ baseURL: 'https://api.mysite.com', badTier: 'prod' } as Options) /** * Case 4: * 'badTier'가 아니라 'tier' * badOptions 변수에 할당되었으므로 신선하지 않음 * 타입스크립트가 에러를 잡지 못한다 */ let badOptions = { baseURL: 'https://api.mysite.com', badTier: 'prod' }; /** * Case 5: * 'badTier'가 아니라 'tier' * options 변수의 타입을 Options라고 명시하면 할당된 객체는 신선한 객체로 취급되므로 에러 잡음 * 초과 프로퍼티 확인은 options를 new API로 전달할 때가 아니라 옵션 객체를 options 변수로 할당할 때 수행됨 */ newAPI(badOptions); let options: Options = { baseURL: 'https://api.mysite.com', badTier: 'prod' } new API(options);
6-1-5. 정제
- 타입스크립트 심벌 수행Symbolic Execution의 일종인 흐름 기반 타입 추론을 수행한다
- 즉, typeof, instanceof, in 등의 타입 질의뿐 아니라, 마치 프로그래머가 코드를 읽듯
if
,?
,||
,switch
같은 제어 흐름 문장까지 고려하여 타입을 정제한다
차별된 유니온 타입
아래 예제의 경우 if 블록 내부에서 typeof로 확인했으므로
event.value
가 문자열임을 알고 있다따라서 if 블록 이후의
event.value
는 [number, number] 타입의 튜플이어야 한다type UserTextEvent = { value: string }; type UserMouseEvent = { value: [number, number] }; type UserEvent = UserTextEvent | UserMouesEvent; function handle(event: UserEvent) { if (typeof event.value === 'string') { event.value // string // ...기타 로직 return; } event.value // [number, number] }
하지만 살짝 더 복잡해진 아래 예제에서는
event.value
는 제대로 정제되지만,event.target
에는 적용되지 않는다- handle 함수가 UserEvent의 타입의 매개변수를 받는다는 것은 UserTextInput이나 UserMouseEvent만 전달할 수 있다는 의미가 아니다. 사실 UserMouseEvent | UserTextEvent 타입의 인수를 전달할 수도 있다
- 유니온의 맴버가 서로 중복될 수 있으므로 더 안정적으로 어떤 타입에 해당하는지 파악할 수 있는 방법이 필요하다
- 리터럴 타입을 이용해 유니온 타입이 만들어낼 수 있는 각각의 경우를 태그하는 방식으로 해당 문제를 해결할 수 있다
type UserTextEvent = { type: 'TextEvent', value: string, target: HTMLInputElement, } type UserMouseEvent = { type: 'MouseEvent', value: [number, number], target: HTMLElement, } type UserEvent = UserTextEvent | UserMouseEvent function handle(event: UserEvent) { if (event.type === 'TextEvent') { event.value // string event.target // HTMLInputElement //... return; } event.value // [number, number] event.target // HTMLElement }
event 태그된 필드(event.type)값에 따라 정제하도록 수정했으므로, 타입스크립트는 if 문에서는 event가 UserTextEvent여야 하며, if문 이후로는 UserMouseEvent여야 한다는 사실을 알게 된다
태그는 유니온 타입에서 고유하므로 타입스크립트는 둘이 상호 배타적임을 알 수 있다
💡 좋은 태그(tag)의 조건 1. 유니온 타입의 각 경우와 같은 위치에 있다 - 객체 타입 유니온에서는 같은 객체 필드, 튜플 타입이라면 같은 인덱스 - 보통 태그된 유니온은 객체 타입을 사용함 2. 리터럴 타입 - 다양한 리터럴 타입을 혼합/매치할 수 있지만, 주로 한 가지 타입만 사용하는 것이 바람직함 - 보통 문자열 사용 3. 제네릭이 아니다 4. 상호 배타적이다 - 유니온 타입 내에서 고유함
6-3. 고급 객체 타입
6-3-1. 객체 타입의 타입 연산자
- 타입스크립트에서는 유니온(|)과 인터섹션(&) 외의 형태 관련 연산을 수행하는 데 도움을 주는 연산자들을 제공한다
A. 키인(Key In) 연산자:
아래 예제처럼 복잡한 중첩 타입이 있고, API 응답을 받아와서
friendList
보여줘야 하는 상황이라면,friendList
의 타입은 어떻게 될까?type APIResponse = { user: { userId: string friendList: { count: number friends: { firstName: string lastName: string }[] } } } function getAPIResponse(): Promise<APIResponse> { // ... } function renderFriendList(friendList: unknown) { // ... }
일반 자바스크립트에서 객체의 필드를 찾는 대괄호 문법과 키인 문법은 같다 (의도적으로 비슷하게 만들어짐!)
키인 연산자를 사용하면 모든 형태와 배열에 키인할 수 있다
type FriendList = APIResponse['user']['friendList']; function renderFriendList(friendList: FriendList) { // ... }
B. keyof 연산자:
keyof
를 이용하면 객체의 모든 키를 문자열 리터럴 타입 유니온으로 얻을 수 있다type ResponseKeys = keyof APIResponse; // 'user' type UserKeys = keyof APIReponse['user']; // 'userId' | 'friendList' type FriendListsKeys = keyof APIRResponse['user']['friendList']; // 'count' | 'friends'
키인과 keyof 연산자를 혼합해 사용하면 객체에서 주어진 키에 해당하는 값을 반환하는 게터를 타입 안전한 방식으로 구현할 수 있다
function get< // 1 O extends object, K extends keyof O // 2 >( o: O, k: K ): O[K] { // 3 return o[k]; }
1.
get은 객체 o와 키 k를 인수로 받는 함수다2.
keyof O는 문자열 리터럴 타입의 유니온으로 o의 모든 키를 표현한다. 예를 들어 o가{ a: number, b: string, c: boolean }
이라면 keyof o는'a' | 'b' | 'c'
타입이 되며, keyof o를 상속받은 K는 'a', 'b', 'a' | 'c', 등 keyof o의 서브타입이 될 수 있다3.
O[K]는 O에서 K를 찾을 때 얻는 타입이다이렇게 타입스크립트에서는 안전하게 타입의 형태를 묘사할 수 있다
6-3-2. Record 타입
TS 내장 Record 타입을 이용하면 무언가를 매핑하는 용도로 객체를 활용할 수 있다
일반 인덱스 시그니처에서는 객체 값의 타입을 제한할 수 있지만, 키는 반드시 일반 string, number, symbol이어야 한다. 하지만 Record에서는 객체의 카 타입도 string과 number의 서브타입으로 제한할 수 있다
💡 추가 노트: - 인덱스 시그니처의 단점은 문자열 리터럴을 key로 사용하는 경우 오류 발생함 - 반면 Record 타입은 문자열 리터럴을 key로 사용하는 것을 허용함
type userInfo = { [name: 'James' | 'Ryan' | 'Jake']: number } // Error: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.(1337) type Names = 'James' | 'Ryan' | 'Jake'; type UserInfo = Record<Names, number>; const userInfo: UserInfo = { 'James': 100, 'Ryan': 95, 'Jake': 90 } // --> OK
6-3-3. 매핑된 타입
매핑된 타입은 Record 보다 강력하다. 객체의 키와 값에 타입을 제공할 뿐 아니라, 키인 타입과 조합하면 키 이름별로 매핑할 수 있는 값 타입을 제한할 수 있기 때문이다 (타입스크립트의 Record 타입 또한 매핑된 타입으로 구현되어 있음)
매핑된 타입에서만 사용할 수 있는 마이너스 연산자
-
로 ?와 readonly 표시를 제거할 수도 있다type Account = { id: number isEmployee: boolean notes: string[] } // 1. 모든 필드를 선택형으로 만듦 type OptionalAccount = { [K in keyof Account]?: Account[K] } // 2. 모든 필드를 nullable로 만듦 type NullableAccount = { [K in keyof Account]: Account[K] | null } // 3. 모든 필드를 readonly로 만듦 type ReadonlyAccount = { readonly [K in keyof Account]: Account[K] } // 4. 모든 필드를 다시 쓸 수 있도록 만듦 (Account와 같음) type Account2 = { -readonly [K in keyof ReadonlyAccount]: Account[K] } // 5. 모든 필드를 다시 필수형으로 만듦 (Account와 같음) type Account3 = { [K in keyof OptionalAccount]-?: Account[K] }
위 예제에서 구현한 것들은 TS에서 내장 타입으로 제공된다
// 💡 내장 매핑된 타입 Record<Keys, Values> // -> Keys 타입의 키와 Values 타입의 값을 갖는 객체 Partial<Object> // -> Object의 모든 필드를 선택형으로 표시 Required<Object> // -> Object의 모든 필드를 필수형으로 표시 Readonly<Object> // -> Object의 모든 필드를 읽기 전용으로 표시 Pick<Object, Keys> // -> 주어진 Keys에 대응하는 Object의 서브타입을 반환
6-3-4. 컴패니언 객체 패턴
같은 이름을 공유하는 객체와 클래스를 쌍으로 연결하는 패턴이다
타입과 값은 별도의 네임스페이스를 갖기 때문에 같은 영역에 하나의 이름을 타입과 값 모두에 연결할 수 있다
type Currency = { unit: 'EUR' | 'GBP' | 'JPY' | 'USD' value: number } const Currency = { DEFAULT: 'USD','; from(value: number, unit = Currency.DEFAULT): Currency { return { unit, value } } }
이 패턴을 이용하면 타입과 값 정보를 Currency 같은 한 개의 이름으로 그룹화할 수 있으며, 호출자는 이 둘을 한 번의 import문으로 가져올 수 있다
import { Currency } from './Currency' // 0. 한 번의 import로 Currency 다 가져옴 const amountDue: Currency = { // 1. Currency를 타입으로 사용 unit: 'JPY' value: 83733.10 } const otherAmountDue = Currency.from(330, 'EUR'); // 2. Currency를 값으로 사용
타입과 객체가 의미상 관련되어 있으며, 객체가 타입을 활용하는 유틸리티 메서드를 제공한다면 컴패니언 객체 패턴을 이용하는 것이 좋다
6-4. 고급 함수 타입들
6-4-1. 튜플의 타입 추론 개선
TS가 나머지 매개변수의 타입을 추론하는 기법을 이용하면 타입 어서션,
as const
을 사용하지 않고, 추론 범위를 좁히지도 않고 튜플을 튜플 타입으로 만들 수 있다function tuple<T extends unknown[]>(...ts: T): T { return ts; }
- tuple 함수는 튜플 타입을 만드는 데 사용하는 함수다
unknown[]
의 서브타입인 단일 타입 매개변수 T를 선언한다 (= T는 모든 종류의 타입을 담을 수 있는 배열임을 의미함)- tuple 함수는 임의 개수의 매개변수 ts를 받는다. T는 나머지 매개변수를 나타내므로 TS는 이를 튜플 타입으로 추론한다
- tuple 함수는 ts의 추론 타입과 같은 튜플 타입의 값을 반환한다
- 함수는 우리가 전달한 인수를 그대로 반환한다
6-4-2. 사용자 정의 타입 안전 장치
때로는 단순히 함수가 boolean 타입의 값을 반환하다고 하고 끝내기 아쉬운 상황이 있다 (아래 예제 참고)
function isString(a: unknown): boolean { return typeof a === 'string'; } function parseInput(input: string | number) { let formattedInput: string if (isString(input)) formattedInput = input.toUpperCase(); // Error! }
타입 정제는 강력한 도구지만, 현재 유효범위에 속한 변수만 처리할 수 있다는 단점이 있다
결국 위 예제에서는 TS가 알고 있는 사실은 isString 이란 함수는 boolean 타입의 값을 반환한다는 것 뿐이다
타입 검사기에 isString이 boolean 타입의 값 뿐 아니라 boolean이 true인 경우 전달한 인수가 string이라는 사실도 알려야 한다
사용자 정의 타입 안전 장치User-defined Type Guard 기법으로 해결 가능하다
function isString(a: unknown): a is string { return typeof a === 'string'; }
typeof, instanceof 타입 정제 내장 기능도 있지만, 자신만의 타입 안전 장치가 필요할 때는
is
연산자를 사용한다
6-5. 조건부 타입
조건부 타입은 타입스크립트에서 제공되는 기능 중 가장 독특하다고 할 수 있다
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // -> true type B = IsString<number>; // -> false
6-5-1. 분배적 조건부
T라는 가변 타입의 인수를 받아 T[]과 같은 배열 타입으로 변환하는 함수가 있다고 가정했을 때, T에 유니온 타입을 전달하면 아래 예저처럼 동작한다
type ToArray<T> = T[]; type A = ToArray<number>; // -> number[] type B = ToArray<number | string>; // -> (number | string)[]
조건부 타입을 사용하면 TS는 유니온 타이블 조건부의 절들로 분배한다
type ToArray<T> = T extends unknown ? T[] : T[]; type A = ToArray<number>; // -> number[] type B = ToArray<number | string>; // -> number[] | string[]
반대로 T에는 존재하지만 U에는 존재하지 않는 타입을 구하는
Without<T, U>
는 아래와 같이 구현해볼 수 있다type Without<T, U> = T extends U ? never : T; type A = Without<boolean | number | string, boolean>; // -> number | string
TS의 Without 수행 로직은 아래와 같다
// 1. 입력 type A = Without<boolean | number | string, boolean>; // 2. 조건을 유니온으로 분배한다 type A = Without<boolean, boolean> | Without<number, boolean> | Without<string, boolean> // 3. Without의 정의를 교체하고 T와 U를 적용함 type A = (boolean extends boolean ? never : boolean) | (number extends boolean ? never : number) | (string extends boolean ? never : string) // 4. 조건 평가 type A = never | number | string; // 5. 단순화 type A = number | string;
6-5-2. infer 키워드
infer
키워드는 조건부 타입에서 제네릭 타입을 인라인으로 선언하는 전용 문법이며, 조건의 일부를 제네릭 타입으로 선언할 수 있게 해준다배열의 요소 타입을 얻는
ElementType
이라는 조건부 타입을 정의해보면 아래와 같다type ElementType<T> = T extends (infer U)[] ? U : T; type A = ElementType<number[]>; // -> number
6-5-3. 내장 조건부 타입들
TS가 제공하는 조건부 타입들은 아래와 같다
// 1. T에 속하지만 U에 없는 타입 구한다 Exclude<T, U> type A = number | string; type B = string; type C = Exclude<A, B>; // --> number // 2. T의 타입 중 U에 할다할 수 있는 타입 구한다 Extract<T, U> type A = number | string; type B = string; type C = Extract<A, B>; // --> string // 3. T에서 null과 undefined를 제외한 버전 구함 NonNullable<T> type A = { a?: number | null }; type B = NonNullable<A['a']>; // --> number // 4. 함수의 반호나 타입을 구한다 (제네릭/오버로드된 함수에서는 동작 X) ReturnType<F> type F = (a: number) => string; type R = ReturnType<F>; // --> string // 5. 클래스 생성자의 인스턴스 타입을 구한다 InstanceType<C> type A = { new(): B }; type B = { b: number }; type I = InstanceType<A>; // { b: number }
6-6. 탈출구
- 상황에 따라 타입을 완벽하게 지정하지 않고도 어떤 작업이 안전하다는 사실을 TS가 믿도록 만들고 싶을 때가 있다
6-6-1. 타입 어서션
TS는 두 개의 타입 어셔선 문법을 제공한다
function formatInput(input: string) { /* 함수 로직 */ } function getUserInput(): string { /* 함수 로직 */ } let input = getUserInput(); // 1. input이 string이라고 어서션 formatInput(input as string); // 2. 위와 같은 의미, 다른 문법 formatInput(<string>input);
6-6-2. Nonnull 어서션
- 어떤 값이 null이나 undefined가 아니라 T임을 단언하는 특수 문법을 TS는 제공한다
type Dialog = {
id?: string
}
function closeDialog(dialog: Dialog) {
if (!dialog.id) return;
setTimeout(() =>
removeFromDOM(
dialog,
document.getElementById(dialog.id) // Error!: 'string | undefined' 타입의 인수는 'string' 타입의 매개변수에 할당할 수 없음
)
)
}
function removeFromDOM(dialog: DIalog, element: Element) {
element.parentNode.removeChild(element) // Error!: 객체가 null일 수 있음
delete dialog.id;
}
- 첫 에러는 화살표 함수 내부로 이동하면서 유효범위가 바뀌게 되면서 발생한 일이다. 함수 첫 줄에 early return문으로 dialog.id가 없는 경우 함수 로직이 실행되지 않도록 했으나, TS는
document.getElementById
를 호출하면 HTMLElement | null 타입의 값을 반환한다는 사실만 알고있다 - 두 번째 에러도 마찬가지로 TS는
element.parentNode
가 Node | null 타입의 값을 반환한다는 사실만 알고있다
이런 경우 TS가 제공하는 Nonnull 어서션 문법(!) 을 활용할 수 있다
type Dialog = { id?: string } function closeDialog(dialog: Dialog) { if (!dialog.id) return; setTimeout(() => removeFromDOM( dialog, document.getElementById(dialog.id)! ) ); } function removeFromDOM(dialog: DIalog, element: Element) { element.parentNode!.removeChild(element); delete dialog.id; }
6-6-3. 확실한 할당 어서션
TS는 확실한 할당 검사(변수를 사용할 때 값이 이미 할당되어 있는지 검사하는 용)를 위해 nonnull 어셔선을 적용하는 특별한 상황에 사용할 특수 문법을 제공한다
function fetchUser() { userId = globalCahce.get('userId'); } let userId!: string; // <- '!'로 userId를 사용하는 시점에 이 변수가 반드시 할당되어 있을 것임을 TS에 알림 fetchUser(); userId.toUpperCase(); // <- 확실한 할당 어서션을 사용하지 않으면 Error (userId 변수에 아무 값도 할당되어 있지 않을 수 있음)
6-7. 이름 기반 타입 흉내내기
TS의 타입 시스템은 당연히 이름이 아니라 구조에 기반하지만, 때로는 이름 기반 타이핑이 유용할 때도 있다
아래 예제를 확인하면 왜 이름 기반 타이핑의 유용함을 확인할 수 있다
// 1. CompanyID와 UserId가 'b4010201' 같은 형태라면 모두 단순히 string의 별칭인 셈이다 type CompanyID = string; type UserID = string; type IDs = CompanyID | UserId; // 2. UserId 타입 값이 필요한 queryForUser 함수 function queryForUser(id: UserID) { /* ... */ } // 3. id 변수에 CompanyID 타입의 값을 할당했지만, Company/UserID 모두 단순히 string의 별칭이므로 함수에서 에러가 발생하지 않는다 let id: CompanyID = 'b4843361'; queryForUser(id); // -> OK (!!!)
TS는 이름 기반 타입을 제공하진 않지만, 타입 브랜딩Type Branding이라는 기법으로 이를 흉내낼 수 있다
// 1. 'unique symbol' type CompanyID = string & { readonly brand: unique symbol }; type UserID = string & { readonly brand: unique symbol }; type IDs = CompanyID | UserID; // 2. 각 브랜디드 타입의 생성자 // - 주어진 id 값을 앞서 정의한 타입들로 지정하는 데 (as) 타입어서션 사용 function CompanyID(id: string) { return id as CompanyID; } function UserID(id: string) { return id as UserID; } // 3. UserID 타입만 받는 queryForUser 함수 function queryForUser(id: UserID) { // ... } // 4. 브랜디드 타입 생성 및 queryForUser 함수 테스트 let companyId = CompanyID('a123123'); let userId = UserId('d129591'); queryForUser(userId); // --> OK! queryForUser(companyId); // --> Error!: CompanyID 타입의 인수를 UserID 타입의 매개변수에 할당할 수 없음
6-8. 프로토타입 안전하게 확장하기
TS의 정적 타입 시스템을 이용하면 프로토타입을 안전하게 확장할 수 있다 (확장하지 않아야 하는 이유는 안정성 외에도 많지만, 안전성은 더이상 확장하지 않음의 이유가 되지 않음)
// 1. 타입스크립트에 zip이 무엇인지 설명하는 단계 (* 인터페이스 합치기) // - Array<T>는 전역 인터페이스, 여기에 zip<U> .. 를 선언 병합함 interface Array<T> { zip<U>(list: U[]): [T, U][] } // 2. Array prototpye에 zip 메서드 구현 // - this 타입을 사용하여 타입스크립트가 zip이 호출되는 대상 배열에서 T타입을 올바르게 추론하도록 함 Array.prototype.zip = function<T, U>( this: T[], list: U[] ): [T, U][] { // 3. TS는 매핑 함수의 반환 타입을 (T | U)[]로 추론하므로 tuple 유틸리티 이용 return this.map((val, idx) => tuple(val, list[idx])) }