2023-05-19 · 15 min read · 학습
러닝 타입스크립트 CHAPTER 15 - 타입 운영
매핑된 타입
타입스크립트는 다른 타입의 속성을 기반으로 새로운 타입을 생성하는 구문을 제공합니다. 즉, 하나의 타입에서 다른 타입으로 매핑합니다. 매핑된 타입은 다른 타입을 가져와서 해당 타입의 각 속성에 대해 일부 작업을 수행하는 타입입니다.
매핑된 타입은 키 집합의 각 키에 대한 새로운 속성을 만들어 새로운 타입을 생성합니다.
type NewType = { [K in OriginalType]: NewProperty}매핑된 타입의 일반적인 예시는 유니언 타입에 존재하는 각 문자열 리터럴 키를 가진 객체를 생성하는 것입니다.
type Animals = 'alligator' | 'baboon' | 'cat'
type AnimalCounts = { [K in Animals]: number}// {// alligator: number;// baboon: number;// cat: number;// }타입에서 매핑된 타입
매핑된 타입은 존재하는 타입에 keyof 연산자를 사용해 키를 가져올 수도 있습니다.
interface AnimalVariants { alligator: boolean baboon: number cat: string}
type AnimalCounts = { [K in keyof AnimalVariants]: number}
// {// alligator: number;// baboon: number;// cat: number;// }매핑된 타입과 시그니처
인터페이스 멤버를 함수로 선언하는 방법에는 두 가지가 있습니다.
- member(): void 같은 메서드 구문: 인터페이스의 멤버가 객체의 멤버로 호출되도록 의도된 함수임을 선언
- member: () => void 같은 속성 구문: 인터페이스의 멤버가 독립 실행형 함수와 같다고 선언
매핑된 타입은 객체 타입의 메서드와 속성 구문을 구분하지 않습니다. 매핑된 타입은 메서드를 원래 타입의 속성으로 취급합니다.
interface Researcher { researchMethod(): void researchProperty: () => string}
type JustProperties<T> = { [K in keyof T]: T[K]}
type ResearcherProperties = JustProperties<Researcher>
// {// researchMethod: () => void;// researchProperty: () => string;// }대부분의 실용적인 타입스크립트 코드에서 메서드와 속성의 차이는 잘 나타나지 않습니다.
제한자 변경
매핑된 타입은 원래 타입의 멤버에 대해 접근 제어 제한자인 readonly와 ?도 변경 가능합니다.
interface Environmentalist { area: string name: string}
type ReadonlyEnvironmentalist = { readonly [K in keyof Environmentalist]: Environmentalist[K]}
// {// readonly area: string;// readonly name: string;// }
type OptionalReadonlyEnvironmentalist = { [K in keyof ReadonlyEnvironmentalist]?: ReadonlyEnvironmentalist[K]}
// {// readonly area?: string | undefined;// readonly name?: string | undefined;// }새로운 타입의 제한자 앞에 -를 추가해 제한자를 제거할 수도 있습니다.
-readonly나 -?을 사용합니다.
제네릭 매핑된 타입
매핑된 타입은 제네릭과 결합해 단일 타입의 매핑을 다른 타입에서 재사용할 수 있을 때 강력한 힘을 발휘합니다.
type MakeReadonly<T> = { readonly [K in keyof T]: T[K]}
interface Species { genus: string name: string}
type ReadonlySpecies = MakeReadonly<Species>
// {// readonly genus: string;// readonly name: string;// }조건부 타입
타입스크립트의 타입 시스템은 이전 타입에 대한 논리적인 검사를 바탕으로 새로운 구성(타입)을 생성합니다. 조건부 타입의 개념은 기존 타입을 바탕을 두 가지 가능한 타입 중 하나로 확인되는 타입입니다.
type CheckStringAgainstNumber = string extends number ? true : false // false제네릭 조건부 타입
조건부 타입에서도 재사용 가능한 제네틱 타입을 적용할 수 있습니다.
type CheckAgainstNumber<T> = T extends number ? true : false
type CheckString = CheckAgainstNumber<'parakeet'> // false
type CheckNumber = CheckAgainstNumber<1981> // true타입 분산
조건부 타입은 유니언에 분산됩니다.
결과 타입은 각 구성 요소에 조건부 타입을 적용하는 유니언이 됨을 의미합니다.
ConditionalType<T | U>는 Conditional<T> | Conditional<U>와 같습니다.
type ArrayifyUnlessString<T> = T extends string ? T : T[]
type HalfArrayified = ArrayifyUnlessString<string | number> // string | number[]유추된 타입
제공된 타입의 멤버에 접근하는 것은 타입의 멤버로 저장된 정보에 대해서는 잘 작동하지만 함수 매개변수 또는 반환 타입과 같은 다른 정보에 대해서는 알 수 없습니다.
조건부 타입은 extends 절에 infer 키워드를 사용해 조건의 임의의 부분에 접근합니다.
type ArrayItems<T> = T extends (infer Item)[] ? Item : T
type StringItem = ArrayItems<string> // string
type StringArrayItem = ArrayItems<string[]> // string
type String2DItem = ArrayItems<string[][]> // string[]매팽된 조건부 타입
메핑된 타입과 조건부 타입을 함께 사용하여 제네릭 템플릿 타입의 각 멤버에 조건부 로직을 적용할 수 있습니다.
type MakeAllMembersFunctions<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : () => T[K]}
type MemberFuntions = MakeAllMembersFunctions<{ alreadyFunction: () => string notYetFunction: number}>
// {// alreadyFunction: () => string;// notYetFunction: () => number;// }never
올바른 위치에 never 타입 애너테이션을 추가하면 타입스크립트가 이전 런타임 예제 코드뿐만 아니라 타입 시스템에서 맞지 않는 코드 경로를 더 공격적으로 탐지합니다.
never와 교차, 유니언 타입
bottom 타입인 never는 존재할 수 없는 타입이라는 의미를 가지고 있습니다.
never가 교차 타입(&)과 유니언 타입(|)을 함께 사용하면 흥미롭게 작동합니다.
- 교차 타입(&)에 있는
never는 교차 타입을never로 만듭니다. - 유니언 타입(|)에 있는
never는 무시됩니다.
type NeverIntersection = never & string // never
type NeverUnion = never | string // string
// 값 필터링never와 조건부 타입
never는 유니언에서 무시되기 때문에 유니언 타입에서 제네릭 조건부의 결과는 never가 아닌 것이 됩니다.
type OnlyStrings<T> = T extends string ? T : never
type RedOrBlue = OnlyStrings<'red' | 'blue' | 0 | false> // 'red' | 'blue'never와 매핑된 타입
유니언에서 never의 동작은 매핑된 타입에서 멤버를 필터링할 때도 유용합니다.
- 유니언에서
never는 무시됩니다. - 매핑된 타입은 타입의 멤버를 매핑할 수 있습니다.
- 조건부 타입은 조건이 충족되는 경우 타입을
never로 변환하는데 사용할 수 있습니다.
type OnlyStringProperties<T> = { [K in keyof T]: T[K] extends string ? K : never}[keyof T]
interface AllEventData { participants: string[] location: string name: string year: number}
type OnlyStringEventData = OnlyStringProperties<AllEventData>템플릿 리터럴 타입
문자열 값을 입력하기 위한 전략으로 두 가지를 제시했습니다.
- 원시 string 타입: 값이 세상의 모든 문자열이 될 수 있는 경우
- ”와 ‘abc’ 같은 리터럴 타입: 값이 오직 한 가지 타입만 될 수 있는 경우
그러나 경우에 따라 문자열이 일부 문자열 패턴과 일치함을 나타내고 싶을 수 있습니다. 이때 타입스크립트 구문으로 템플릿 리터럴 타입을 사용할 수 있습니다.
type Greeting = `Hello${string}`let matches: Greeting = 'Hello, world' // OK템플릿 리터럴 타입을 더 좁은 문자열 패턴으로 제한하기 위해 포괄적인 string 원시 타입 대신 문자열 리터럴 타입과 유니언을 타입 보간법에 사용할 수 있습니다.
type Brightness = 'dark' | 'light'type Color = 'blue' | 'red'
type BrightnessAndColor = `${Brightness}-${Color}` // 'dark-blue' | 'dark-red' | 'light-blue' | 'light-red'
let colorOk: BrightnessAndColor = 'dark-blue' // OK고유 문자열 조작 타입
문자열 타입 작업을 지원하기 위해 타입스크립트는 고유 제네릭 유틸리티 타입을 제공합니다.
- Uppercase: 문자열 리터럴 타입을 대문자로 변환
- Lowercase: 문자열 리터럴 타입을 소문자로 변환
- Capitalize: 문자열 리터럴 타입의 첫 번째 문자를 대문자로 변환
- Uncapitalize: 문자열 리터럴 타입의 첫 번째 문자를 소문자로 변환
템플릿 리터럴 키
템플릿 리터럴 타입은 원시 문자열 타입과 문자열 리터럴 사이의 중간 지점이므로 여전히 문자열입니다. 템플릿 리터럴 타입은 문자열 리터럴을 사용할 수 있는 모든 위치에서 사용 가능합니다.
type DataKey = 'location' | 'name' | 'year'
type ExistenceChecks = { [K in `check${Capitalize<DataKey>}`]: () => boolean}
// {// checkLocation: () => boolean;// checkName: () => boolean;// checkYear: () => boolean;// }매핑된 타입 키 다시 매핑하기
타입스크립트는 템플릿 리터럴 타입을 사용해 원래 멤버를 기반으로 매핑된 타입의 멤버에 대한 새로운 키를 생성할 수 있습니다.
매핑된 타입에서 인덱스 시그니처에 대한 템플릿 리터럴 타입 다음에 as 키워드를 배치하면 결과 타입의 키는 템플릿 리터럴 타입과 일치하도록 변경됩니다.
이렇게 하면 매핑된 타입은 원래 값을 계속 참조하면서 각 매핑된 타입 속성에 대한 다른 키를 가질 수 있습니다.
interface DataEntry<T> { key: T value: string}
type DataKey = 'location' | 'name' | 'year'
type DataEntryGetters = { [K in DataKey as `get${Capitalize<K>}`]: () => DataEntry<K>}
// {// getLocation: () => DataEntry<"location">;// getName: () => DataEntry<"name">;// getYear: () => DataEntry<"year">;// }키를 다시 매핑하는 작업과 다른 타입 운영을 결합해 기존 타입 형태를 기반으로 하는 매핑된 타입을 생성하는 것입니다.
자바스크립트에서 객체 키는 string 또는 symbol이 될 수 있고,
symbol 키는 원시 타입이 아니므로 템플릿 리터럴 타입으로 사용할 수 없습니다.
type TurnIntoGettersDirect<T> = { [K in keyof T as `get${K}`]: () => T[K]}
// Type 'K' is not assignable to type 'string | number | bigint | boolean | null | undefined'.// Type 'keyof T' is not assignable to type 'string | number | bigint | boolean | null | undefined'.// Type 'string | number | symbol' is not assignable to type 'string | number | bigint | boolean | null | undefined'.// Type 'symbol' is not assignable to type 'string | number | bigint | boolean | null | undefined'.이러한 제한 사항을 피하기 위해 string과 교차 타입(&)을 사용하여 문자열이 될 수 있는 타입만 사용하도록 강제합니다.
string & symbol은 never가 되므로 전체 템플릿 문자열은 never가 되고 타입스크립트는 이를 무시하게 됩니다.
const someSymbol = Symbol('')
interface HasStringAndSymbol { StringKey: string [someSymbol]: number}
type TurnIntoGetters<T> = { [K in keyof T as `get${string & K}`]: () => T[K]}
type GettersJustString = TurnIntoGetters<HasStringAndSymbol>
// {// getStringKey: () => string;// }타입 운영과 복잡성
애초에 디버깅은 코드를 작성하는 것보다 두 배나 더 어렵습니다. 따라서 코드를 가능한 한 영리하게 작성하는 사람일지라도, 디버그할 정도로 똑똑하지는 않습니다.
마치며
- 기존 타입을 새로운 타입으로 변환하기 위해 매핑된 타입 사용하기
- 조건부 타입을 사용해서 타입 운영에 로직 도입하기
- 교차, 유니언, 조건부 타입, 매핑된 타입과
never가 상호작용하는 방법 배우기 - 템플릿 리터럴 타입을 사용해서 문자열 타입의 패턴 나타내기
- 타입 키를 수정하기 위해 템플릿 리터럴 타입과 매핑된 타입 결합하기