Learning TypeScript - Chap. 15 Type Operations

이소라·2023년 7월 21일
0

TypeScript

목록 보기
28/28

15.1 Mapped Types

  • TypeScript의 매핑된 타입(mapped type)은 다른 타입을 가져와서 해당 타입의 각 속성에 일부 작업을 수행하는 타입입니다.
    • 매핑된 타입은 키 집합의 각 키에 대한 새로운 속성을 만들어 새로운 타입을 생성합니다.
    • 매핑된 타입은 [K in OriginalType]과 같이 in을 사용해 다른 타입으로부터 계산된 타입을 사용합니다.
type NewType = {
  [K in OriginalType]: NewProperty;
};
  • 매핑된 타입에 대한 일반적인 사용 사례는 union 타입에 존재하는 각 문자열 리터럴 키를 가진 객체를 생성하는 것입니다.
type Animals = "alligator" | "baboon" | "cat";

type AnimalCounts = {
  [K in Animals]: number;
}
// {
//		alligator: number;
//		baboon: number;
//		cat: number;
//}
  • 존재하는 union 리터럴을 기반으로 매핑된 타입은 큰 interface를 선언하는 공간을 절약하는 편리한 방법입니다.

  • 매핑된 타입은 다른 타입에 대해 작동하고 멤버에서 제한자를 추가하거나 제거할 수 있을 때 정말로 유용해집니다.


15.1.1 Mapped Types from Types

  • 일반적으로 매핑된 타입은 존재하는 타입에 keyof 연산자를 사용해 키를 가져오는 방식으로 작동합니다.
    • 존재하는 타입의 키를 매핑하도록 지시하면 새로운 타입으로 매핑됩니다.
interface AnimalVariants {
  alligator: boolean;
  baboon: number;
  cat: string;
}

type AnimalCounts = {
  [K in keyof AnimalVariants]: number;
};
// {
//		alligator: number;
//		baboon: number;
//		cat: number;
//}
  • 각 매핑된 멤버 타입 값은 동일한 키 아래에서 원래 타입의 해당 멤버 값을 참조할 수 있습니다.
    • 원본 객체가 SomeName이고 매핑이 [K in keyof SomeName]인 경우 매핑된 타입의 각 멤버는 SomeName 멤버의 값을 SomeName[k]로 참조할 수 있습니다.
interface BirdVariants {
  dove: string;
  eagle: boolean;
}

type NullableBirdVariants = {
  [K in keyof BirdVariants]: BirdVariants[K] | null;
};
// {
//		dove: string | null;
//		eagle: boolean | null;
//}
  • 매핑된 타입은 멤버 집합을 한 번 정의하고 필요한 만큼 여러 번 새로운 버전을 다시 생성할 수 있습니다.

매핑된 타입과 시그니처

  • TypeScript가 interface 멤버를 함수로 선언하는 두 가지 방법을 제공합니다.

    • member(): void 같은 메서드 구문 : interface 멤버가 객체의 멤버로 호출되도록 의도된 함수임을 선언
    • member: () => void 같은 속성 구문 : interfece 멤버가 독립 실행형 함수와 같다고 선언
  • 매핑된 타입은 객체 타입의 메서드와 속성 구문을 구분하지 않습니다.

    • 매핑된 타입은 메서드를 원래 타입의 속성으로 취급합니다.
interface Researcher {
  researchMethod(): void;
  researchProperty: () => string;
}

type JustProperties<T> = {
  [K in keyof T]: T[K];
};

type ResearcherProperties = JustProperties<Researcher>;
// {
//		researcherMethod: () => void;
//		researcherProperty: () => string;
// }
  • 대부분의 실용적인 TypeScript 코드에서 메서드와 속성의 차이는 잘 나타나지 않습니다.
    • 클래스 타입을 갖는 매핑된 타입을 실제로 사용하는 경우는 매우 드문 일입니다.

15.1.2 Changing Modifiers

  • 매핑된 타입은 원래 타입에 대한 멤버에 접근 제어 제한자인 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;
//		readonly name?: string;
// }

type OptionalReadonlyEnvironmentalist2 = {
  readonly [K in keyof Environmentalist]?: Environmentalist[K]; 
};
// {
//		readonly area?: string;
//		readonly name?: string;
// }
  • 새로운 타입의 제한자 앞에 -를 추가해 제한자를 제거할 수 있습니다.
interface Conservationist {
  name: string;
  catchphrase?: string;
  readonly born: number;
  readonly died?: number;
}

type WritableConservationist = {
  -readonly [K in keyof Conservationist]: Conservationist[K];
};
// {
//		name: string;
//		catchphrase?: string | undefined;
//		born: number;
//		died?: number | undefined;
// }

type RequiredWritableConservationist = {
 [K in keyof WritableConservationist]-?: Conservationist[K];
};
// {
//		name: string;
//		catchphrase?: string;
//		born: number;
//		died?: number;
// }

type RequiredWritableConservationist2 = {
  -readonly [K in keyof Conservationist]-?: Conservationist[K];
};
// {
//		name: string;
//		catchphrase?: string;
//		born: number;
//		died?: number;
// }

15.1.3 Generic Mapped Types

  • 매핑된 타입의 완전한 힘은 제네릭과 결합해 단일 타입의 매핑을 다른 타입에서 재사용할 수 있도록 하는 것에서 나옵니다.
    • 제네릭 매핑된 타입은 매핑된 타입 자체의 타입 매개변수를 포함해 keyof로 해당 스코프에 있는 모든 타입 이름에 접근할 수 있습니다.
    • 제네릭 매핑된 타입은 애플리케이션을 통해 흐를 때 데이터가 어떻게 변형되는지 나타낼 때 아주 유용합니다.
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;
// }
  • 제네릭 매핑된 타입을 사용하여 임의의 수의 interface를 받고, 그 interface의 완전히 채워진 인스턴스를 반환하는 함수를 구현할 수 있습니다.
interface GenusData {
  family: string;
  name: string;
}

type MakeOptional<T> = {
  [K in keyof T]?: T[K];
}

type OptionalGenusData = MakeOptional<GenusData>;
// {
//		family?: string | undefined;
//		name?: string | undefined;
// }

function createGenusData(overrides?: OptionalGenusData): GenusData {
  // GenusData의 기본값('unknown') 위에 모든 {overrides}를 구조 분해 할당함
  return {
    family: 'unknown',
    name: 'unknown',
    ...overrides,
  }
}
  • 제네릭 매핑된 타입으로 수행되는 일부 작업은 매우 유용하므로 TypeScript는 제네릭 매핑된 타입을 즉시 사용할 수 있는 유틸리티 타입을 제공합니다.
    • 예를 들어, 내장된 Partial<T> 타입을 사용해 모든 속성을 선택 사항으로 만들 수 있습니다.



15.2 Conditional Types

  • TypeScript의 타입 시스템은 논리 프로그래밍 언어의 한 예입니다.

    • TypeScript의 타입 시스템은 이전 타입에 대한 논리적인 검사를 바탕으로 새로운 구성(타입)을 생성합니다.
    • 조건부 타입(conditional type)의 개념은 기존 타입을 바탕으로 두 가지 가능한 타입 중 하나로 확인되는 타입입니다.
  • 조건부 타입 구문은 삼항 연산자 조건문처럼 보입니다.

LeftType extends RightType ? IfTrue : IfFalse
  • 조건부 타입에서 논리적 검사는 항상 extends의 왼쪽 타입이 오른쪽 타입이 되는지 또는 할당 가능한지 여부에 있습니다.
// 타입 : false
type CheckStringAgainstNumber = string extends number ? true : false;
  • 조건부 타입은 boolean로직의 일부입니다.
    • 조건부 타입은 두 가지 가능한 결과 중 하나를 얻습니다.

15.2.1 Generic Conditional Types

  • 조건부 타입은 조건부 타입 자체의 타입 매개변수를 포함한 해당 스코프에서 모든 타입 이름을 확인할 수 있습니다.
    • 즉, 모든 다른 타입을 기반으로 새로운 타입을 생성하기 위해 재사용 가능한 제네릭 타입을 작성할 수 있습니다.
type CheckAgainstNumber<T> = T extends number ? true : false;

// 타입 : false
type CheckString = CheckAgainstNumber<'parakeet'>;
// 타입 : true
type CheckNumber = CheckAgainstNumber<1891>;
// 타입 : true
type CheckNumberType = CheckAgainstNumber<number>;
  • 다음 CallableSetting 타입은 제네릭 T를 받고 T가 함수인지 아닌지 확인합니다.
    • T가 함수인 경우, 결과 타입은 T가 됩니다.
    • T가 함수가 아닌 경우, 결과 타입은 T를 반환하는 함수가 됩니다.
type CallableSetting<T> = T extends () => any ? T : () => T

// 타입 : () => number[]
type GetNumbersSetting = CallableSetting<() => number[]>;
// 타입 : () => string
type StringSetting = CallableSetting<string>;
  • 조건부 타입은 객체 멤버 검색 구문을 사용해서 제공된 타입의 멤버에 접근할 수 있고, extends 절과 결과 타입에서 그 정보를 사용할 수 있습니다.

  • JavaScript 라이브러리에서 사용하는 패턴 중 조건부 제네릭 타입에도 적합한 한 가지 패턴은 함수에 제공된 옵션 객체를 기반을 함수의 반환 타입을 변경하는 것입니다.

    • 예를 들어 대부분의 데이터베이스 함수나 이와 동등한 함수는 값을 찾을 수 없는 경우 undefined를 반환하는 대신 throwIfNotFound와 같은 속성을 사용해 함수가 오류를 발생시키도록 변경할 수 있습니다.
interface QueryOptions {
  throwIfNotFound: boolean;
}

type QueryResult<Options extends QueryOptions> = 
Options["throwIfNotFound"] extends true ? string : string | undefined;

declare function retrieve<Options extends QueryOptions>(
key: string,
 options?: Options
): Promise<QueryResult<Options>>;

// 반환된 타입 : string | undefined
await retrieve("Birute Galdikas");
// 반환된 타입 : string | undefined
await retrieve("jane Goodall", { throwIfNotFound: Math.random() > 0.5 });
// 반환된 타입 : string
await retrieve("Dian Fossey", { throwIfNotFound: true });
  • 조건부 타입을 제네릭 타입 매개변수와 결합하면 retrieve 함수는 프로그램의 제어 흐름을 어떻게 변경할 것인지를 타입 시스템에 더 정확히 알릴 수 있습니다.

15.2.2 Type Distributivity

  • 조건부 타입은 union에 분산(distribute)됩니다.
    • 결과 타입은 각 구성 요소(union 타입 안의 타입들)에 조건부 타입을 적용하는 union이 됨을 의미합니다.
    • 즉, ConditionalType<T | U> === Conditional<T> | Conditional<U>와 같습니다.
type ArrayifyUnlessString<T> = T extends string ? T : T[];

// 타입 : ArrayifyUnlessString<string> | ArrayifyUnlessString<number>
// 타입 : string | number[]
type HalfArrayified = ArrayifyUnlessString<string | number>;
  • TypeScript의 조건부 타입이 union에 분산되지 않는다면 string | number는 string에 할당할 수 없기 때문에 HalfArrayified는 (string | number)[]가 됩니다.
    • 즉, 조건부 타입은 전체 union 타입이 아니라 union 타입의 각 구성 요소에 로직을 적용합니다.

15.2.3 Inferred Types

  • 제공된 타입의 멤버에 접근하는 것은 타입의 멤버로 저장된 정보에 대해서는 잘 작동하지만 함수 매개변수 또는 반환 타입과 같은 다른 정보에 대해서는 알 수 없습니다.

  • 조건부 타입은 extends 절에 infer 키워드를 사용해 조건의 임의의 부분에 접근합니다.

    • extends 절에 타입에 대한 infer 키워드와 새 이름을 배치하면 조건부 타입이 true인 경우 새로운 타입을 사용할 수 있음을 의미합니다.
  • 다음 ArrayItems 타입은 타입 매개변수 T를 받고 T가 새로운 Item 타입의 배열인지 확인합니다.

    • 새로운 Item 타입의 배열인 경우 결과 타입은 Item이 되고, 그렇지 않으면 T가 됩니다.
type ArrayItems<T> = T extends (infer Item)[] ? Item : T;

// 타입 : string
type StringItem = ArrayItems<string>;
// 타입 : string
type StringArrayItem = ArrayItems<string[]>;
// 타입 : string[]
type String2DItem = ArrayItems<string[][]>;
  • 유추된 타입은 재귀적 조건부 타입을 생성하는 데에도 사용할 수 있습니다.

  • 다음 ArrayItemsRecursive는 이전의 ArrayItems 타입이 모든 차원 배열의 item 타입을 재귀적으로 검색하도록 확장했습니다.

type ArrayItemsRecursive<T> = T extends (infer Item)[] ? ArrayItemsRecursive<Item> : T;

// 타입 : string
type StringItem = ArrayItemsRecursive<string>;
// 타입 : string
type StringArrayItem = ArrayItemsRecursive<string[]>;
// 타입 : string
type String2DItem = ArrayItemsRecursive<string[][]>;
  • 유추된 타입은 제네릭 타입이 재귀적일 수 있는 기능을 통해 변경 사항을 계속 적용할 수 있습니다.

15.2.4 Mapped Conditional Types

  • 매핑된 타입은 기존 타입의 모든 멤버에 변경 사항을 적용하고 조건부 타입은 하나의 기존 타입에 변경 사항을 적용합니다.

    • 이 둘을 함께 사용하면 제네릭 템플릿 타입의 각 멤버에 조건부 로직을 적용할 수 있습니다.
  • 다음 MakeAllMembersFunctions 타입은 함수가 아닌 각 멤버를 함수로 바꿉니다.

type MakeAllMembersFunctions<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : () => T[K];
};

type MemberFunctions = MakeAllMembersFunctions<{
  alreadyFunction: () => string,
  notYetFunction: number,
}>;
// {
//		alreadyFunction: () => string;
//		notYetFunction: () => number;
// }
  • 매핑된 조건부 타입은 일부 논리적 검사를 사용해 기존 타입의 모든 속성을 수정하는 편리한 방법입니다.



15.3 never

  • never 타입은 가능한 값을 가질 수 없고 접근할 수 없음을 의미합니다.
    • 올바른 위치에 never type annotation을 추가하면 TypeScript가 타입 시스템에서 맞지 않는 코드 경로를 더 공격적으로 탐지합니다.

15.3.1 never and Intersections and Unions

  • bottom 타입인 never는 존재할 수 없는 타입이라는 의미를 가지고 있습니다.
  • never가 insersection 타입(&)과 union 타입(|)을 함께 사용하면 다음과 같이 작동합니다.
    • intersection 타입(&)에 있는 never는 intersection 타입을 never로 만듭니다.
    • union 타입(|)에 있는 never는 무시됩니다.
// 타입 : never
type NeverIntersection = never & string;
// 타입 : string
type NeverUnion = never | string;
  • 특히 union 타입에서 never가 무시되는 동작은 조건부 타입과 매핑된 타입에서 값을 필터링하는 데 유용합니다.

15.3.2 never and Conditional Types

  • 제네릭 조건부 타입은 일반적으로 union에서 타입을 필터링하기 위해 never를 사용합니다.
    • never는 union에서 무시되기 때문에 union 타입에서 제네릭 조건부의 결과는 never가 아닌 것이 됩니다.
type OnlyStrings<T> = T extends string ? T : never;

// 타입 : "red" | "blue"
type RedOrBlue = OnlyStrings<"red" | "blue" | 0 | false>;
  • never는 또한 제네릭 타입에 대한 타입 유틸리티를 만들 때 유추된 조건부 타입과 결합됩니다.

    • infer가 있는 타입 추론은 조건부 타입이 true가 되어야 하므로 false인 경우를 절대 사용하지 않아야 합니다.
    • 바로 이때 never를 사용하면 적합합니다.
  • 다음 FirstParameter 타입은 함수 타입 T를 받고, arg: infer Arg가 함수인지 확인하고, 함수가 맞다면 Arg를 반환합니다.

    • 조건부 타입의 false에 never를 사용하면 FirstParameter가 함수의 첫 번째 매개변수 타입을 추출할 수 있습니다.
type FirstParameter<T extends (...args: any[]) => any> = 
T extends (arg : infer Arg) => any ? Arg : never;

// 타입 : string
type GetsString = FirstParameter<(arg0: string) => void>;

15.3.3 never and Mapped Types

  • union에서 never의 동작은 매핑된 타입에서 멤버를 필터링할 때도 유용합니다.

  • 다음 세 가지 타입 시스템 기능을 사용해 객체의 키를 필터링합니다.

    • union에서 never는 무시됩니다.
    • 매핑된 타입은 타입의 멤버를 매핑할 수 있습니다.
    • 조건부 타입은 조건이 충족되는 경우 타입을 never로 변환하는데 사용할 수 있습니다.
  • 세 가지 기능을 함께 사용하면 원래 타입의 각 멤버를 원래 키 또는 never로 변경하는 매핑된 타입을 만들 수 있습니다.

    • [keyof T]로 해당 타입의 멤버를 요청하면 모든 매핑된 타입의 결과 union이 생성되고 never는 필터링됩니다.
  • 다음 OnlyStringProperties 타입은 각 T[K] 멤버가 string인 경우 K 키로 변경하고, string이 아닌 경우 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;
}

// 타입 : "location" | "name"
type OnlyStringEvent = OnlyStringProperties<AllEventData>;



15.4 Template Literal Types

  • 템플릿 리터럴 타입(template literal type)은 문자열 타입이 패턴에 맞는지를 나타내는 TypeScript 구문입니다.
    • 템플릿 리터럴 타입은 템플릿 리터럴 문자열처럼 보이지만 추정할 수 있는 원시 타입 또는 원시 타입 union이 있습니다.
type Greeting = `Hello${string}`;

// Ok
let matches: Greeting = "Hello, world!";
// Error: Type "World! Hello!" is not assignable to type `Hello${string}`
let outOfOrders: Greeting = "World! Hello!";
// Error: Type "hi" is not assignable to type `Hello${string}`
let missingAlTogether: Greeting = "hi";
  • 템플릿 리터럴 타입을 더 좁은 문자열 패턴으로 제한하기 위해 포괄적인 string 원시 타입 대신 문자열 리터럴 타입과 그 union을 타입 보간법(type interpolation)에 사용할 수 있습니다.
    • 템플릿 리터럴 타입은 제한된 허용 문자열 집합과 일치해야 하는 문자열을 설명하는 데 매우 유용합니다.
type Brightness = "dark" | "light";
type Color = "blue" | "red";

// 타입: type Brightness = "dark-blue" | "dark-red" | "light-blue" | "light-red"
type BrightnessAndColor = `${Brightness}-${Color}`;

// Ok
let colorOk: BrightnessAndColor = "dark-blue";

// Error: Type '"medium-blue"' is not assignable to type 
// 	'"dark-blue" | "dark-red" | "light-blue" | "light-red"'.
let colorWrongStart: BrightnessAndColor = "medium-blue";

// Error: Type '"light-green"' is not assignable to type 
// 	'"dark-blue" | "dark-red" | "light-blue" | "light-red"'.
let colorWrongEnd: BrightnessAndColor = "light-green";
  • TypeScript는 템플릿 리터럴 타입이 string, number, boolean, null, undefined와 같은 모든 원시 타입(symbol 제외) 또는 그 조합을 포함하도록 허용합니다.
type ExtolNumber = `much ${number} wow`;

function extol(extolee: ExtolNumber) { /* ... */ }

// Ok
extol('much 0 wow');
// Ok
extol('much -7 wow');
// Ok
extol('much 9.001 wow');
// Error: Argument of type '"much false wow"' is not 
// 	assignable to parameter of type '`much ${number} wow`'.
extol('much false wow');

15.4.1 Intrinsic String Manipulation Types

  • 문자열 타입 작업을 지원하기 위해 TypeScript는 문자열을 가져와 문자열에 일부 조작을 적용하는 고유 (TypeScript에 내장된) 제네릭 유틸리티 타입을 제공합니다.
    • Uppercase : 문자열 리터럴 타입을 대문자로 변환합니다.
    • Lowercase : 문자열 리터럴 타입을 소문자로 변환합니다.
    • Capitalize : 문자열 리터럴 타입의 첫 번째 문자를 대문자로 변환합니다.
    • Uncapitalize : 문자열 리터럴 타입의 첫 번째 문자를 소문자로 변환합니다.
  • 각각은 문자열을 갖는 제네릭 타입으로 사용할 수 있습니다.
type FormalGreeting = Capitalize<"hello.">; // 타입 : "Hello."
  • 이러한 고유 문자열 조작 타입은 객체 타입의 속성 키를 조작하는 데 매우 유용합니다.

15.4.2 Template Literal Keys

  • 템플릿 리터럴 타입은 원시 문자열 타입과 문자열 리터럴 사이의 중간 지점이므로 여전히 문자열입니다.
    • 템플릿 리터럴 타입은 문자열 리터럴을 사용할 수 있는 모든 위치에서 사용 가능합니다.
    • 예를 들어 매핑된 타입의 인덱스 시그니처로 사용할 수 있습니다.
type DataKey = "location" | "name" | "year";

type ExistenceChecks = {
  [K in `check${Capitalize<DataKey>}`]: () => boolean;
};

// {
//		checkLocation: () => boolean;
//		checkName: () => boolean;
//		checkYear: () => boolean;
// }

function checkExistence(checks: ExistenceChecks) {
	checks.checkLocation(); // 타입 : boolean
	checks.checkName(); // 타입 : boolean
	// Error: Property 'checkWrong' does not exist on type 'ExistenceChecks'.
	checks.checkWrong();
}

15.4.3 Remapping Mapped Type Keys

  • TypeScript는 템플릿 리터럴을 사용해 원래 멤버를 기반으로 매핑된 타입의 멤버에 대한 새로운 키를 생성할 수 있습니다.
    • 매핑된 타입에서 인덱스 시그니처에 대한 템플릿 리터럴 타입 다음에 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">;
// }
  • 키를 다시 매핑하는 작업과 다른 타입 운영을 결합해 기존 타입 형태를 기반으로 하는 매핑된 타입을 생성할 수 있습니다.
    • 기존 객체에 keyof typeof를 사용해 해당 객체의 타입에서 매핑된 타입을 만들 수 있습니다.
const config = {
  location: "unknown",
  name: "anonymous",
  year: 0,
};

type LazyValues = {
  [K in keyof typeof config as `${K}Lazy`]: () => Promise<typeof config[K]>;
};

// {
//		location: Promise<string>;
//		name: Promise<string>;
//		year: Promise<number>;
// }                                                     
                                                         
async function withLazyValues(configGetter: LazyValues) {
  // 타입 : string
  await configGetter.locationLazy;
  // Error: 
const config = {
  location: "unknown",
  name: "anonymous",
  year: 0,
};

type LazyValues = {
  [K in keyof typeof config as `${K}Lazy`]: () => Promise<typeof config[K]>;
};

// {
//		location: Promise<string>;
//		name: Promise<string>;
//		year: Promise<number>;
// }                                                     
                                                         
async function withLazyValues(configGetter: LazyValues) {
  // 타입 : string
  await configGetter.locationLazy;
  // Error: Property 'missingLazy' does not exist on type 'LazyValues'.
  await configGetter.missingLazy;
}
  • JavaScript에서 객체 키는 string 또는 symbol이 될 수 있고, symbol 키는 원시 타입이 아니므로 템플릿 리터럴 타입으로 사용할 수 없습니다.
  • 제네릭 타입에서 다시 매핑된 템플릿 리터럴 타입 키를 사용하려고 하면 TypeScript는 템플릿 리터럴 타입에서 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과 intersection 타입(&)을 사용하여 문자열이 될 수 있는 타입만 사용하도록 강제합니다.
    • string & symbolnever가 되므로 전체 템플릿 문자열은 never가 되고 TypeScript는 이를 무시하게 됩니다.
const someSymbol = Symbol("");

interface HasStringAndSymbol {
  StringKey: string;
  [someSymbol]: number;
}

type TurnIntoGetters<T> = {
  [K in keyof T as `get${string & K}`]: () => T[K]
};

type GetterJustString = TurnIntoGetters<HasStringAndSymbol>;

const someSymbol = Symbol("");

interface HasStringAndSymbol {
  StringKey: string;
  [someSymbol]: number;
}

type TurnIntoGetters<T> = {
  [K in keyof T as `get${string & K}`]: () => T[K]
};

type GetterJustString = TurnIntoGetters<HasStringAndSymbol>;
// {
//		getStringKey: () => string;
// }



15.5 Type Operations and Complexity

  • 타입 운영을 사용해야 하는 경우에는 향후 코드를 읽어야 하는 모든 개발자를 위해 가능한 한 최소한으로 사용하도록 노력하세요.
  • 코드를 읽는 사람이 이해하기 쉬운 이름을 사용하고, 미래에 코드를 읽을 때 어려움을 겪을 수 있다고 생각되는 모든 부분에 설명을 남겨주세요.

0개의 댓글