일.인덱스 타입(Index types)
인덱스 타입은 정적 검사가 타입이 불확정 (열거할 수 없는) 인"동적"시나리오를 커버할 수 있게 합니다. 예를 들어:
function pluck(o, names) {
return names.map(n => o[n]);
}
pluck 함수는 o 에서 names 가 지정한 부분의 프로퍼티를 꺼낼 수 있습니다. 2 개의 타입 제약이 있습니다:
-
파라미터
names중에는o에 있는 프로퍼티만 나타날 수 있음 -
반환 타입은 파라미터
o의 프로퍼티 값의 타입에 의존
이들 2 개의 제약은 모두 제네릭을 통해 기술할 수 있습니다:
interface pluck {
<T, K extends keyof T>(o: T, names: K[]): T[K][]
}
let obj = { a: 1, b: '2', c: false };
// 파라미터 검사
// 错误 Type 'string' is not assignable to type '"a" | "b" | "c"'.
pluck(obj, ['n']);
// 반환 타입 추론
let xs: (string | number)[] = pluck(obj, ['a', 'b']);
P.S.interface 는 함수 타입을 기술 가능, 상세는 二.함수 참조
2 개의 새로운 것이 나타났습니다:
-
keyof: 인덱스 타입 쿼리 연산자(index type query operator) -
T[K]: 인덱스 액세스 연산자(indexed access operator):
인덱스 타입 쿼리 연산자
keyof T 는 타입 T 위의 모든 public 프로퍼티명을 취득하여 유니온 타입을 구성합니다. 예를 들어:
// 等价于 let t: { a: number; b: string; c: boolean; }
let t: typeof obj;
// 等价于 let availableKeys: "a" | "b" | "c"
let availableKeys: keyof typeof obj;
declare class Person {
private married: boolean;
public name: string;
public age: number;
}
// 等价于 let publicKeys: "name" | "age"
let publicKeys: keyof Person;
P.S. 주의, typeof 가 값面向인 것과는 달리, keyof 는 타입面向 입니다. 값이 아닙니다 (따라서 keyof obj 는 불법)
이 타입 쿼리 능력은 pluck 등 사전에得知할 수 없는 (또는 열거할 수 없는) 프로퍼티명의 시나리오에서 매우 의의가 있습니다
인덱스 액세스 연산자
keyof 와 유사하며, 또 하나의 타입 쿼리 능력은 인덱스로 타입에 액세스하는 것(T[K])으로, 타입 레벨의 프로퍼티 액세스 연산자 에 상당합니다:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
let c: boolean = getProperty(obj, 'c');
// 等价于
let cValue: typeof obj['c'] = obj['c'];
즉, 만약 t: T, k: K 라면, t[k]: T[K] 입니다:
type typesof<T, K extends keyof T> = T[K];
let a: typesof<typeof obj, 'a'> = obj['a'];
let bOrC: typesof<typeof obj, 'b' | 'c'> = obj['b'];
bOrC = obj['c'];
// 错误 Type 'number' is not assignable to type 'string | boolean'.
bOrC = obj['a'];
인덱스 타입과 문자열 인덱스 서명
keyof 와 T[K] 는 문자열 인덱스 서명(index signature) 에도 동일하게 적용됩니다. 예를 들어:
interface NetCache {
[propName: string]: object;
}
// string | number 타입
let keyType: keyof NetCache;
// object 타입
let cached: typesof<NetCache, 'http://example.com'>;
keyType 의 타입은 string | number 임을 알 수 있습니다. 예상된 string 이 아닌데, 이는 JavaScript 에서 수치 인덱스는 문자열 인덱스로 변환되기 때문입니다. 예를 들어:
let netCache: NetCache;
netCache[20190101] === netCache['20190101']
즉, key 의 타입은 문자열이어도 수치여도 되며, 즉 string | number 입니다. 만약 강하게 number 를剔除하고 싶다면, 내장된 Extract 타입 별칭을 통해 완성할 수 있습니다:
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
(TypeScript/lib/lib.es5.d.ts 에서 발췌)
let stringKey: Extract<keyof NetCache, string> = 'http://example.com';
물론, 일반적으로 이렇게 할 필요는 없습니다. 타입 각도에서 보면, key: string | number 는 합리적이기 때문입니다
P.S. 더 많은 관련 논의는, Keyof inferring string | number when key is only a string 참조
이.매핑 타입
인덱스 타입과 유사하며, 기존 타입에서 새로운 타입을 파생시키는 또 하나의 방법은 매핑을 수행하는 것입니다:
In a mapped type, the new type transforms each property in the old type in the same way.
예를 들어:
type Stringify<T> = {
[P in keyof T]: string
}
// 모든 프로퍼티 값을 toString()一遍
function toString<T>(obj: T): Stringify<T> {
return Object.keys(obj)
.reduce((a, k) =>
({ ...a, [k]: obj[k].toString() }),
Object.create(null)
);
}
let stringified = toString({ a: 1, b: 2 });
// 错误 Type 'number' is not assignable to type 'string'.
stringified = { a: 1 };
Stringify 는 { [propName: string]: any } 에서 { [propName: string]: string } 로의 타입 매핑을 구현했지만, 그다지 유용해 보이지는 않습니다. 실제, 더 일반적인 용법은 매핑 타입을 통해 key 의 속성을 변경하는 것입니다. 예를 들어 어떤 타입의 모든 프로퍼티를 옵션 또는 읽기 전용으로 만드는 것:
type Partial<T> = {
[P in keyof T]?: T[P];
}
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
(TypeScript/lib/lib.es5.d.ts 에서 발췌)
let obj = { a: 1, b: '2' };
let constObj: Readonly<typeof obj>;
let optionalObj: Partial<typeof obj>;
// 错误 Cannot assign to 'a' because it is a read-only property.
constObj.a = 2;
// 错误 Type '{}' is missing the following properties from type '{ a: number; b: string; }': a, b
obj = {};
// 正确
optionalObj = {};
구문 포맷
가장 직관적인 예:
// 하나의"타입집"을 찾음
type Keys = 'a' | 'b';
// 타입 매핑을 통해 새로운 타입 { a: boolean, b: boolean } 을 얻음
type Flags = { [K in Keys]: boolean };
[K in Keys] 형식상은 인덱스 서명과 유사하지만, for...in 구문을 융합했습니다. 그 중에서:
-
K: 타입 변수, 순서대로 각 프로퍼티에 바인드되며, 각 프로퍼티명의 타입에 대응 -
Keys: 문자열 리터럴로 구성되는 [유니온 타입](/articles/조합 타입과 타입 보호-typescript 노트 9/#articleHeader3), 한 조의 프로퍼티명(의 타입)을 나타냄 -
boolean: 매핑 결과 타입, 즉 각 프로퍼티 값의 타입
유사하게, [P in keyof T] 는 keyof T 를(프로퍼티명)타입집으로 찾아,从而에 기존 타입을 매핑하여 새로운 타입을 얻습니다
P.S.另外, Partial 와 Readonly 는 모두 원 타입 정보를 완전히 보존할 수 있습니다 (입력된 원 타입에서 프로퍼티명 및 값 타입을 취득하고, 수식자상의 차이만 존재하며, 원 타입과 새로운 타입 간에 호환 관계가 있습니다). 동태(homomorphic) 변환이라고 불리며, Stringify 는 원 프로퍼티 값 타입을丢弃하고, 비동태(non-homomorphic)변환에 속합니다
"開箱"추론(unwrapping inference)
타입을 매핑하는 것은타입 레벨의"装箱" 에 상당합니다:
// 包装 타입
type Proxy<T> = {
get(): T;
set(value: T): void;
}
// 装箱(普通 타입 to 包装 타입의 타입 매핑)
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
// 装箱 함수
function proxify<T>(o: T): Proxify<T> {
let result: Proxify<T>;
// ... wrap proxies ...
return result;
}
예를 들어:
// 普通 타입
interface Person {
name: string,
age: number
}
let lily: Person;
// 装箱
let proxyProps: Proxify<Person> = proxify(lily);
동様に, "開箱"도 할 수 있습니다:
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps: Person = unproxify(proxyProps);
최종 행의 unproxify 함수 타입을 자동으로 추론할 수 있습니다:
function unproxify<Person>(t: Proxify<Person>): Person
파라미터 타입 proxyProps: Proxify<Person> 에서 Person 을 꺼내어 반환값 타입으로 하고, 소위"開箱"입니다
삼.조건부 타입
조건부 타입은 비균일 타입 매핑(non-uniform type mapping)을 표현하는 데 사용되며, 타입 호환 관계(즉조건)에 기반하여 2 개의 타입에서 1 개를 선택할 수 있습니다:
T extends U ? X : Y
의미는 삼항 연산자와 유사하며, 만약 T 가 U 의 서브타입이라면 X 타입,否则 Y 타입입니다.另外, 또 하나의 상황은 조건의 진위가 확정할 수 없는(T 가 U 의 서브타입인지 확정할 수 없는)경우로,此時 X | Y 타입입니다. 예를 들어:
declare function f<T extends boolean>(x: T): T extends true ? string : number;
// x 의 타입为 string | number
let x = f(Math.random() < 0.5)
另外, 만약 T 또는 U 가 타입 변수를 포함한다면, 타입 변수가 모두 대응하는 구체적인 타입을 가져야 조건부 타입의 결과를 얻을 수 있습니다:
When T or U contains type variables, whether to resolve to X or Y, or to defer, is determined by whether or not a the type system has enough information to conclude that T is always assignable to U.
예를 들어:
interface Foo {
propA: boolean;
propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;
function foo<U>(x: U) {
// a 의 타입为 U extends Foo ? string : number
let a = f(x);
let b: string | number = a;
}
그 중에서 a 의 타입은 U extends Foo ? string : number(즉 조건이 불확정한 상황)이며, f(x) 중 x 의 타입 U 가 아직 확정되지 않아, U 가 Foo 의 서브타입인지得知할 수 없습니다. 그러나 조건부 타입无非 2 개의 가능 타입이므로, let b: string | number = a; 는 반드시 합법입니다(x 가 무슨 타입이어도)
분배 가능 조건부 타입
분배 가능 조건부 타입(distributive conditional type)中被검사된 타입은 벌거벗은 타입 파라미터(naked type parameter)입니다. 그 특수한 점은 분배법칙을 만족한다는 것입니다:
(A | B | C) extends U ? X : Y
等价于
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
예를 들어:
// 嵌套의 조건부 타입은 패턴 매칭과 유사
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" : "object";
// T 타입等价于유니온 타입 string" | "function
type T = TypeName<string | (() => void)>;
另外, T extends U ? X : Y 중, X 중에 나타나는 T 는 모두 U 타입 제약을 가집니다:
type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
// T 타입等价于유니온 타입 BoxedValue<string> | BoxedArray<boolean>
type T = Boxed<string | boolean[]>;
상예 중 Boxed<T> 의 True 분기는 any[] 타입 제약을 가지므로, 인덱스 액세스(T[number])를 통해 배열 요소의 타입을 얻을 수 있습니다
응용 시나리오
조건부 타입은 매핑 타입과 결합하여针对性的인 타입 매핑을 실현할 수 있습니다 (다른 원 타입은 다른 매핑 규칙에 대응 가능). 예를 들어:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
// 모든 함수 타입의 프로퍼티를摘出
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
// T 타입等价于문자열 리터럴 타입 "updatePart"
type T = FunctionPropertyNames<Part>;
而분배 가능 조건부 타입은 일반적으로 유니온 타입을篩選하는 데 사용됩니다:
type Diff<T, U> = T extends U ? never : T;
// T 타입等价于유니온 타입 "b" | "d"
type T = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
// 더 한걸음 나아간
type NeverNullable<T> = Diff<T, null | undefined>;
function f1<T>(x: T, y: NeverNullable<T>) {
x = y;
// 错误 Type 'T' is not assignable to type 'Diff<T, null>'.
y = x;
}
조건부 타입 중의 타입 추론
조건부 타입의 extends 절中, infer 선언을 통해 추론되는 타입 변수를 도입할 수 있습니다. 예를 들어:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
상예 중 타입 변수 R 을 도입하여 함수 반환 타입을 나타내고, True 분기中에서 인용하여,从而에 반환 타입을 추출합니다
P.S. 特殊的, 만약 오버로드가 존재한다면, 마지막 서명을 취합니다 (관례에 따라, 마지막은 일반적으로 가장 광범위) 하여 추론합니다. 예를 들어:
declare function foo(x: string): number;
declare function foo(x: number): string;
declare function foo(x: string | number): string | number;
// T 타입等价于유니온 타입 string | number
type T = ReturnType<typeof foo>;
P.S. 더 많은 예는 Type inference in conditional types 참조
사전 정의의 조건부 타입
TypeScript 는 더욱 몇 가지常用的인 조건부 타입을 내장하고 있습니다:
// T 에서 U 에 속하는 서브타입의 부분을去掉, 즉之前의 예中의 Diff
type Exclude<T, U> = T extends U ? never : T;
// T 에서 U 에 속하는 서브타입의 부분을篩選,之前의 예中의 Filter
type Extract<T, U> = T extends U ? T : never;
// T 에서 null 과 undefined 부분을去掉
type NonNullable<T> = T extends null | undefined ? never : T;
// 함수 타입의 반환 타입을 꺼냄
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
// 构造函数 타입의 예시 타입을 꺼냄
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
(TypeScript/lib/lib.es5.d.ts 에서 발췌)
구체 예는 Predefined conditional types 참조
사.정리
[타입 조합](/articles/조합 타입과 타입 보호-typescript 노트 9/#articleHeader1) 외에, 새로운 타입을 생성하는 또 다른 2 가지 방법은 타입 쿼리와 타입 매핑입니다
타입 쿼리:
- 인덱스 타입: 기존 타입의 일부를 꺼내어 새로운 타입을 생성
타입 매핑:
- 매핑 타입: 기존 타입을 매핑하여 새로운 타입을 얻음
- 조건부 타입: 타입 호환 관계를 조건으로 간단한 삼항 연산을 수행하는 것을 허용하며, 비균일 타입 매핑을 표현하는 데 사용
아직 댓글이 없습니다