TypeScript$Type-Manipulation-Produce
如何基于已有类型 / 值来生成类型呢?
1. The keyof type operator
可以通过 keyof 获取对象类型的 keys 作为类型。
type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y'
index signature:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number: number 会自动装换成 string,所以 number 也行
type DatePropertyNames = keyof Date
type DateStringPropertyNames = DatePropertyNames & string
type DateSymbolPropertyNames = DatePropertyNames & symbol
2. typeof Type Operator
如果想通过值 value 来获取类型 type,可以使用 typeof。
需要注意的是,typeof ClassA 获取的是 function 的类型——即函数作为对象的类型。
let s = "hello";
let n: typeof s; // string
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
/* type P = {
x: number;
y: number;
} */
3. Indexed Access Type
对于对象类型,我们可以通过索引获取其属性的类型。语法是Type[index],其中 Type 是对象类型,index 是类型:可以是 "name" 这样的字面量类型,也可以是 number、 number | string 这样的类型及其组合。
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // number
type I1 = Person["age" | "name"];
type I2 = Person[keyof Person];
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
需要了解的是:
- 索引不存在的属性会报错
- index 是类型,不能使用变量来进行索引
const key = "age";
type Age = Person[key];
/* Error. Type 'key' cannot be used as an index type.
'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'? */
type key = "age";
type Age = Person[key];
Indexed Access Type 实战
对于数组,indexed access type 可以通过 number 获取元素的类型。需要注意的是,如果数组元素类型不同,得到的是 |。
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person = typeof MyArray[number];
/* type Person = {
name: string;
age: number;
} */
type Age = typeof MyArray[number]["age"];
// type Age = number
// Or
type Age2 = Person["age"];
4. Conditional Types
条件类型指的是一个类型可能有多种情况,具体哪种情况需要根据输入的情况来确认。表现形式为:SomeType extends OtherType ? TrueType : FalseType;
上面的说明中,多种情况指的是 TrueType 和 FalseType。具体的类型取决于输入 SomeType 是否 extends OtherType。
条件类型主要用在泛型上。因为泛型可以是任何类型,那么 someType extends OtherType 的结果就会根据泛型的不同而不同(someType 是泛型)。
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
上面使用函数重载的。下面使用泛型+条件类型:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript"); // NameLabel
let b = createLabel(2.8); // IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // NameLabel | IdLabel
可以看到,使用函数重载,为了支持 number | string 的情况,我们需要描述多种情况。而使用泛型+条件类型后,我们只需要一种类型:条件类型。我们把各种类型的情况的逻辑移到了条件类型本身的逻辑中,从而使外面的逻辑更简单。
Conditional Type Constraints
通过对 extends OtherType 中的 OtherType 进行约束,我们可以获取约束里的内容:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>; // string
type DogMessageContents = MessageOf<Dog>; // never
在上面的例子中,我们获取了 { message: unknown } 里的 message 的信息。
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type.
type Str = Flatten<string[]>; // string
// Leaves the type alone.
type Num = Flatten<number>; // number
Inferring Within Conditional Types
从约束条件里提取类型是一种很常见的操作,TypeScript 提供了语法糖:通过 infer 我们可以在 true 分支里获取约束条件里的类型。
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type Flatten<T> = T extends any[] ? T[number] : T;
实战
我们可以使用 infer 关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数类型中提取返回类型:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<(x: string) => string>; // string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]
对于有多个调用签名的类型(如重载函数),会对最后一个签名版型推断:
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
type T1 = ReturnType<typeof stringOrNum>; // string | number
Distributive Conditional Types
Distributive Conditional Types 分布式条件类型。当条件类型作用于泛型类型时,它们在给定联合类型时变得可分配。🤨。简单来说,默认情况下,联合类型是先单独分开,再进行联合。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
为避免这种行为,可以用方括号将 extends 关键字的每一侧括起来。(联合类型被当做不可分割)
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>; // (string | number)[]
5. Mapped Types
使用 index signatures,我们可以定义一个对象的一些未知的属性对应的类型。
映射类型 Mapped types 也类似,只不过 index signatures 是直接定义未知属性,而 mapped types 是通过泛型来获取这些属性。就像是通过泛型的 keys 来设置新的 types。
// index signatures:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
// mapped types:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type Features = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<Features>;
/* type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
} */
Mapping Modifiers
对于 readonly 和 ?,我们可以通过 - 和 + 来删除/添加这些 modifiers。(默认 +)
// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
/* type UnlockedAccount = {
id: string;
name: string;
} */
// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
/* type User = {
id: string;
name: string;
age: number;
} */
Key Remapping via as
有时候我们想改变 keys,这时候可以使用 as:
- 结合 template literal types 使用,可改变 keys 的名字
- 提供
never,可以删除对应的属性
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
/* type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
} */
// Remove the 'kind' property
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
/* type KindlessCircle = {
radius: number;
} */
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>
/* type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
} */
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
/* type ObjectsNeedingGDPRDeletion = {
id: false;
name: true;
} */
6. Template Literal Types
模板字符串类型 template literal types 和 JavaScript 的 模板字符类似,只使用在了类型上。
type World = "world";
type Greeting = `hello ${World}`; // "hello world"
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`; // 所有组合
String Unions in Types
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
// makeWatchedObject has added `on` to the anonymous Object
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
/// Create a "watched object" with an `on` method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
Inference with Template Literals
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
// (parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
});
person.on("ageChanged", newAge => {
// (parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
Intrinsic String Manipulation Types
Uppercase<StringType>Lowercase<StringType>Capitalize<StringType>Uncapitalize<StringType>