「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」
前言
在掌握好 TypeScript 基础之后,就需要将基础类型联合起来做出更优美的类型声明。
TypeScript 中有很多关键字,如果不去主动接触的话就会少了很多可操作性。例如:infer、keyof、typeof、extends 之类的。还有映射和模板字符串这种概念
泛型
利用泛型来达到类型复用的目的,而不是一昧的用 any 大法。这里达到的效果是传入什么类型,就返回该类型
function identify<T>(arg: T): T {
return arg;
}
接口的形式定义泛型
interface GenericIdentifyFn {
<T>(arg: T): T
}
function identify<T>(arg: T): T {
return arg
}
let myIdentify: GenericIdentifyFn = identify
在类型的继承中使用
class BeeKeeper {
hasMask!: boolean;
}
class ZooKeeper {
nametag!: string;
}
class Animal {
numLegs!: number;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
keyof
运算符:构造一个类型,类型由参数的所有键值
interface Person {
name: string;
age: number;
}
type T1 = keyof Person; // "name" | "age"
type T2 = keyof { [x: string]: 1 }; // string | number
type T3 = number;
type T4 = keyof T3; // type T4 = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
提取键值,组成类型。提取一定要是的类型
typeof
运算符:获取变量的类型
type Predicate = (x: unknown) => boolean;
function f() {
return true;
}
type K = ReturnType<Predicate>; // type K = boolean
type T = ReturnType<typeof f>; // type T = boolean
JavaScript 中也有 typeof。TypeScript 环境下,编辑器能够区分应该使用哪个
keyof 和 typeof 这两个都是提取类型的,一个是提取类型,一个是提取变量。可以一起记忆
索引
对于对象,通过属性名索引
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // type Age = number
对于数组,通过数值索引
const MyArray = [
{ name: "Alice", age1: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person = NonNullable<typeof MyArray[number]["age1"]>; // type Person = number
索引的方式可以抽离我们想要的类型
条件类型
条件类型有助于描述输入和输出类型之间的关系,操作起来更加灵活
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
type Example2 = RegExp extends Animal ? number : string;
上面这个是 extends 的条件用法,类似三元表达式语法
重载函数的例子
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
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 | number>(idOrName: T): NameOrId {
thorw "unimplemented";
}
条件类型约束
更好的限制泛型
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
type Flatten<T> = T extends any[] ? T[number] : T;
在条件类型中进行推断 infer
注意:只有在条件类型 extends 子句中才允许 infer 声明
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return ? Return : never
type Num = GetReturnType<() => number>;
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
主要体现了 infer 的用法,作为辅助类型的方式出现
分配条件类型
当我们向条件类型传入联合类型,如果没有使用 [] 会出现映射联合类型的每个成员类型。
type ToArray<T> = T extends any ? T[] : never;
type ToArray2<T> = [T] extends [any] ? T[] : never;
type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]
type StrArrOrNumArr2 = ToArray2<string | number>; // (string | number)[]
映射类型
当我们想要将另一个类型的属性都拿过来的时候,可以用到映射的概念。keyof 拿出所有类型,in 表示在这些类型中
type Horse = {};
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
映射修饰符
readonly 和 ? 可以在映射的过程中使用。用 - + 表示减少或添加,默认 +
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 | undefined;
// name?: string | undefined;
// }
通过 as 来重新设置键名
as 可以将键名设置成我们想要的形式,或者通过 never 过滤
联合模式
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;
// }
这个很好用,也很好玩
模板字符串
类似 js 中的模板字符串语法。遇到联合类型会算出每种组合方式
type World = "world";
type Greeting = `hello ${World}`;
// type Greeting = "hello world"
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
类型中的字符串联合
意思是将类型的属性拿出来进行字符串拼接,可以得到一个联合类型。
type PropEventSource<Type> = {
on(
eventName: `${string & keyof Type}Changed`,
callback: (newValue: any) => void
): void;
};
declare function makeWatchedObject<Type>(
obj: Type
): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
person.on("firstNameChanged", (newValue: any) => {
console.log(`firstName was changed to ${newValue}!`);
});
额,好像没啥不好理解的
使用模板文字进行推理
用模板字符串中的变量来做泛型的类型推断
内在字符串操作类型
intrinsic 是一个关键字,是一种编译
intrinsic 关键字只能用于声明编译器提供的内部类型。
Uppercase<StringType>
将字符串中的每个字符转换为大写版本
type Greeting = "Hello, world";
type ShoutyGreeting = Uppercase<Greeting>;
// type ShoutyGreeting = "HELLO, WORLD"
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<"my_app">;
// type MainID = "ID-MY_APP"
源码
type Uppercase<S extends string> = intrinsic;
Lowercase<StringType>
将字符串中的每个字符转换为小写版本
type Greeting = "Hello, world";
type QuietGreeting = Lowercase<Greeting>;
// type QuietGreeting = "hello, world"
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`;
type MainID = ASCIICacheKey<"MY_APP">;
// type MainID = "id-my_app"
源码
type Lowercase<S extends string> = intrinsic;
Capitalize<StringType>
将字符串中的第一个字符转换为等效的大写字符
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// type Greeting = "Hello, world"
源码
type Capitalize<S extends string> = intrinsic;
Uncapitalize<StringType>
将字符串中的第一个字符转换为等效的小写字符
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// type UncomfortableGreeting = "hELLO WORLD"
源码
type Uncapitalize<S extends string> = intrinsic;
小结
类型操作让我们更加灵活的使用类型,优化很多繁琐的声明。对于每个知识点都需要熟记,毕竟存在即合理,肯定有它的妙用所在