在 js 中 void 是一个操作符,它可以让任何表达式返回 undefined, undefined 不是一个保留字
// 可以对undefined进行声明赋值
(function () {
const undefined = 0;
console.log(undefined);
})();
使用 void 0 则则表示 undefined
never: 一个函数如果抛出了一个错误,则它永远不会有返回值,则返回值类型为 never
// never
let error = () => {
throw new Error("error");
};
interface List {
id: number;
name: string;
}
interface Result {
data: List[];
}
function fn(data: Result) {}
const result = {
data: [
{ id: 1, name: "jj" },
{ id: 2, name: "cc" },
],
};
使用过程中返回的数据可能会增加一些属性,例如
const result = {
data: [
{ id: 1, name: "jj", desc: "test" },
{ id: 2, name: "cc" },
],
};
fn(result); // ts并没有报错
ts 并没有报错 ts 采取了一种鸭式变形法
如果传入对象字面量的话,ts 就会对额外的字段进行检查
fn({
data: [
{ id: 1, name: "jj", desc: "test" }, // 会报错 desc
{ id: 2, name: "cc" },
],
});
这种情况需要使用类型断言
例如 1 建议使用 1,因为下面(例 2)这种在 React 中可以产生歧义
fn({
data: [
{ id: 1, name: "jj", desc: "test" }, // 会报错 desc
{ id: 2, name: "cc" },
],
} as Result);
例如 2
fn(<Result>{
data: [
{ id: 1, name: "jj", desc: "test" }, // 会报错 desc
{ id: 2, name: "cc" },
],
});
// 用任意字符串索引 得到字符串
interface Item {
[index: string]: string;
}
这样声明之后,就不能再声明一个 number 类型的成员了,如
interface Item {
[index: string]: string;
key: number; //报错
}
两种签名是可以混用的,所以 还可以在 Item 中增加一个数字索引签名 如
interface Item {
[index: string]: string;
[key: number]: string; //如果是 number就会报错, 如果上面是any 那么number就可以
}
function fn(x: number, y: number) {
return x + y;
}
let myFn = (x: number, y: number) => number;
type MyFn = (x: number, y: number) => number;
interface MyFn {
(x: number, y: number): number;
}
interface MixinFn {
(): void; //表示这个类是个函数,返回值是void
version: string;
toDo(): void;
}
MixinFn 的实现
let lib:MixinFn = (()=>{}) as MixinFn
lib.version='1.0.0'
lib.toDo = ()=>
函数重载指的是在同一个类中定义多个同名函数,但它们的参数列表不同(参数类型、参数个数或参数顺序)。编译器根据调用时提供的参数信息,来确定应该调用哪个函数。函数重载可以提供不同的方法签名,使得同一个函数名可以用于处理多个不同的情况。
class A {
sum(a: number, b: number) {
return a + b;
}
}
class B extends A {
sub(a: number, b: number) {
return a - b;
}
}
function maker(): B;
function maker(p: string): A;
function maker(p?: string) {
if (p) {
return new A();
}
return new B();
}
function maker(p?: string) {
if (p) {
return new A();
}
return new B();
}
使用断言使编译器通过检查,不过不推荐这种写法,而应该使用重载
type NumGenerator1 = () => number;
function myFunc1(numGenerator1: NumGenerator1 | undefined) {
const num1 = numGenerator1!();
const num2 = numGenerator1!();
}
type NumGenerator2 = () => number;
function myFunc2(): undefined;
function myFunc2(numGenerator2?: NumGenerator2) {
if (numGenerator2) {
const num1 = numGenerator2();
const num2 = numGenerator2();
}
}
type NumGenerator3 = () => number;
function myFunc3(numGenerator3: NumGenerator3 | undefined) {
const num1 = numGenerator3?.();
}
ts 的类覆盖了 js 的类,同时加入了一些新特性
无论在 ts 中还是 es 中,类成员的属性,都是实例属性,而不是原型属性,类成员的方法,都是实例方法。
class Cat {
constructor(name: string) {
this.name = name;
}
name: string;
run() {}
}
继承
// 构造函数中一定要调用super
class ChildCat extends Cat {
constructor(name: string, public color: string) {
super(name);
}
}
除了类成员可以添加修饰符之外,构造函数的参数也可以添加修饰符,它的作用就是将参数自动变成实例的属性,这样就省略在类中的定义了
class ChildCat extends Cat {
constructor(name: string, public color: string) {
super(name);
}
}
static,类的静态修饰符,静态成员也可以被继承
es 中并没有抽象类,这是 ts 对类的拓展,所谓抽象类,就是只能被继承而不能实例化的类。在抽象类中可以定义一个方法,它可以有具体的实现,这样子类就不用实现了,就实现了方法的复用。在抽象类中也可以不指定具体的方法实现,这就构成了抽象方法。抽象方法的好处就是你明知道子类中有其他的实现,那就没必要在父类中实现了。
abstract class Animal {
eat() {
console.log("eat");
}
abstract sleep(): void;
}
class Chicken extends Animal {
sleep() {
console.log("sleep");
}
}
const chicken = new Chicken();
chicken.eat();
链式调用的核心就在于调用完的方法将自身的实例返回
class WorkFlow {
step1() {
return this;
}
step2() {
return this;
}
}
new WorkFlow().step1().step2();
class Myflow extends WorkFlow {
next() {
return this;
}
}
new Myflow().next().step1().next().step2();
一个接口可以约束一个类成员有哪些属性以及他们的类型
interface Human {
name: string;
eat(): void;
}
类实现接口的时候必须实现接口中声明的所有属性和方法
接口只能约束类的公有成员
interface Human {
name: string;
eat(): void;
}
class Asian implements Human {
constructor(name: string) {
this.name = name;
}
name: string;
eat() {}
}
接口可以像类一样相互继承,并且一个接口可以继承多个接口
// Human ==> name eat
interface Man extends Human {
run(): void;
}
interface Child {
cry(): void;
}
interface Boy extends Man, Child {}
const boy: Boy = {
name: "",
run() {},
eat() {},
cry() {},
};
从接口的继承可以看出,接口的继承可以抽离出可重用的接口,也可以将多个接口合并成一个接口
接口除了可以继承接口之外,还可以继承类,这就相当于接口把类的成员都抽象出来,也就是只有类的成员结构,而没有具体的实现
class Auto {
state = 1;
}
interface AutoInterface extends Auto {}
这样 AutoInterface 接口就隐含了 state 属性,要想实现这个 AutoInterface 接口只要一个类的成员和 state 的属性就可以了
class C implements AutoInterface {
state = 1;
}
class Bus extends Auto implements AutoInterface {}
// 在这个例子中我们就不必实现 state 属性了,因为 Bus 是 Auto 的子类自然继承了 state 属性 。这里需要额外注意的是,接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员。
很多时候我们希望一个函数或者一个类可以支持多种数据类型,有很大的灵活性
function print(value: string): string {
console.log(value);
return value;
}
// 重载
function print(value: string): string;
function print(value: string[]): string[];
function print(value: any) {
console.log(value);
return value;
}
// 联合类型
function print(value: string | string[]): string | string[] {
console.log(value);
return value;
}
// any类型
function print(value: any) {
console.log(value);
return value;
}
不预先确定类型,在使用的时候再确定
function log<T>(value: T): T {
console.log(value);
return value;
}
log<string[]>(["a", "b"]);
log(["a", "b"]);
我们不仅可以用泛型来定义一个函数,也可以定义一个函数类型
type Log = <T>(value: T) => T;
const myLog: Log = log;
这个和类型别名的定义方式是等价的,但目前这个泛型仅约束了一个函数,也可以用泛型来约束其他成员
interface Log1 {
<T>(value: T): T;
}
这样接口的所有成员都受到了泛型的约束
这里需要注意的是当泛型约束了整个接口之后,在实现的时候,我们必须指定一个类型
let myLog1: Log2<number> = log;
myLog1(1);
如果不指定类型也可以在接口的定义中指定一个默认类型
interface Log3<T = string> {
(value: T): T;
}
let myLog2: Log3 = log;
myLog2("s");
把泛型变量和函数的参数等同对待,泛型只不过是另一个维度的参数,是代表类型而不是代表值的参数
函数和类可以轻松地支持多种类型,增强程序的拓展性
不必写多条函数重载,冗长的联合类型声明,增强代码可读性
灵活控制类型之间的约束
class Ame<T> {
run(value: T) {
console.log(value);
return value;
}
}
const ame = new Ame<number>();
ame.run(1);
当不指定类型参数的时候,value 值就可以是任意的值
const ame1 = new Ame();
ame1.run({ a: 1 });
ame1.run("ss");
interface Length {
length: number;
}
function print<T extends Length>(value: T): T {
console.log(value, value.length);
return value;
}
// /参数需要具有length属性
print([1]);
print("ss");
print({ length: 1 });
TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。
interface AmeFoo {
name: string;
}
let ameFoo = {} as AmeFoo;
ameFoo.name = "coboy";
推荐在声明的时候就指定类型 即:
let ameFoo1: AmeFoo = { name: "coboy" };
类型断言可以增加我们代码的灵活性,在改造一些旧代码的时候非常有效,但使用类型断言,要注意避免滥用,要对上下文的环境要有充足的预判,没有任何根据的类型断言会带来安全的隐患,总之 TS 的类型推断可以为我们提供重要的辅助信息,应该善加利用
当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
X 兼容 Y:X(目标类型)= Y(源类型)
之所以我们要讨论类型兼容性问题,是因为 TS 允许我们把一些类型不同的变量相互赋值。类型兼容性的例子广泛存在于接口、函数和类中。
interface AmeX {
name: any;
age: any;
}
interface AmeY {
name: any;
age: any;
height: any;
}
let ameX: AmeX = { name: "coboy", age: 25 };
let ameY: AmeY = { name: "cobyte", age: 25, height: 180 };
ameX = ameY;
ameY = ameX; // Property 'height' is missing in type 'AmeX' but required in type 'AmeY'
这里再次体现了 TS 的检测原则,也就是鸭式变形法(一只鸟走起来像鸭子,游起来像鸭子,叫起来像鸭子,就可以被认为是鸭子)
源类型必须具备目标类型的必要属性,就可以进行赋值(成员少的会兼容成员多的)
要判断两个函数是否兼容通常发生在相互赋值的情况下,也就是函数作为参数的情况下
type Handler = (x: number, y: number) => void;
function add(handler: Handler) {
return handler;
}
let handler1 = (x: number) => {};
add(handler1);
let handler2 = (x: number, y: number, z: number) => {};
add(handler2); // Argument of type '(x: number, y: number, z: number) => void' is not assignable to parameter of type 'Handler'
let fun1 = (p1: number, p2: number) => {};
let fun2 = (p1?: number, p2?: number) => {};
let fun3 = (...args: number[]) => {};
fun1 = fun2;
fun1 = fun3;
fun2 = fun3; // error
fun2 = fun1; // error
// 可以将strictFunctionTypes设置为false
fun3 = fun1;
fun3 = fun2;
let handler3 = (a: string) => {};
add(handler3); // error
interface Point3D {
x: number;
y: number;
z: number;
}
interface Point2D {
x: number;
y: number;
}
let p3d = (point: Point3D) => {};
let p2d = (point: Point2D) => {};
p3d = p2d;
p2d = p3d; // error
// 可以将strictFunctionTypes设置为false
这种函数的参数之间可以赋值的情况,叫做函数参数的双向协变,这种情况允许我们把一个精确的类型赋值给一个不那么精确的类型,这样做我们就不需要把一个不精确的类型断言成一个精确的类型。
ts 要求我们目标的返回值类型必须与源函数的返回值类型相同,或者为其子类型
let fun4 = () => ({ name: "coboy" });
let fun5 = () => ({ name: "cobyte", age: 18 });
fun4 = fun5;
fun5 = fun4; // error
function overload(x: number, y: number): number;
function overload(x: string, y: string): string;
function overload(x: any, y: any): any {}
函数重载分为两部分,第一部分就是函数重载的列表,第二部分就是函数的具体实现,这里列表中的函数就是目标函数,而具体的实现函数就是源函数。程序在运行的时候,编译器会查找重载列表,然后使用第一个匹配的定义来执行下面的函数,所以在重载列表中,目标函数的参数要多于源函数的参数,而且返回值类型也要符合相应的要求
枚举类型和数字类型是可以完全相互兼容的
枚举之间是完全不兼容的
enum Fruit {
Apple,
Banana,
}
enum Color {
Red,
Yellow,
}
let fruit: Fruit.Apple = 3;
let no: number = Fruit.Apple;
class AmeByte1 {
constructor(x: number, y: number) {}
id: number = 1;
}
class AmeByte2 {
static x = 1;
constructor(p: number) {}
id: number = 2;
}
let amebyte1 = new AmeByte1(1, 2);
let amebyte2 = new AmeByte2(1);
amebyte1 = amebyte2;
amebyte2 = amebyte1;
类的兼容性和接口的比较相似,他们也只是比较结构。注意:在比较两个类是否兼容的时候,静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那么他们的实例就可以互相兼容。
如果两个类含有私有成员,那么这两个类就不兼容了,这个时候只有父类和子类之间是互相兼容的。
interface Empty1<T> {}
let obj1: Empty1<number> = {};
let obj2: Empty1<string> = {};
obj1 = obj2;
interface Empty2<T> {
value: T;
}
let obj3: Empty2<number> = {}; // error
let obj4: Empty2<string> = {}; // error
obj3 = obj4; // error
只有类型参数 T 被接口成员使用的时候,才会有影响泛型的兼容性
let ameT1 = <T>(x: T): T => {
console.log("x");
return x;
};
let ameT2 = <U>(y: U): U => {
console.log("y");
return y;
};
ameT1 = ameT2;
如果两个泛型函数的定义相同但没有指定类型参数,那么他们之间也是可以互相兼容的。
TypeScript 能够在特定的区块中保证变量属于某种确定的类型,可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
enum Type {
Strong,
Week,
}
class Java {
helloJava() {
console.log("hello Java");
}
java: any;
}
class JavaScript {
helloJavaScript() {
console.log("hello JavaScript");
}
javascript: any;
}
function isJava(lang: Java | JavaScript): lang is Java {
return (lang as Java).helloJava !== undefined;
}
function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
if ((lang as Java).helloJava) {
(lang as Java).helloJava();
} else {
(lang as JavaScript).helloJavaScript();
}
return lang;
}
getLanguage(Type.Strong);
function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
if (lang instanceof Java) {
lang.helloJava();
} else {
lang.helloJavaScript();
}
return lang;
}
getLanguage(Type.Strong);
function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
if ("java" in lang) {
lang.helloJava();
} else {
lang.helloJavaScript();
}
return lang;
}
getLanguage(Type.Strong);
function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
if (typeof x === "string") {
x.length;
} else {
x.toFixed(2);
}
return lang;
}
getLanguage(Type.Strong);
function isJava(lang: Java | JavaScript): lang is Java {
return (lang as Java).helloJava !== undefined;
}
function getLanguage(type: Type, x: string | number) {
let lang = type === Type.Strong ? new Java() : new JavaScript();
if (isJava(lang)) {
lang.helloJava();
} else {
lang.helloJavaScript();
}
return lang;
}
getLanguage(Type.Strong);
所谓交叉类型就是将多个类型合并为一个类型,新的类型具有所有类型的特性,所以交叉类型特别适合对象混入的场景。
interface DogInterface {
run(): void;
}
interface CatInterface {
jump(): void;
}
let pet: DogInterface & CatInterface = {
run() {},
jump() {},
};
需要注意的是交叉类型看名称给人的感觉是几个类型的交集,实际上是取所有类型的并集。
所谓联合类型就是指声明的类型并不确定,可以为多个类型中的一个。
let ameType: number | string = "1"; // 可以等于数字也可以等于字符串
有的时候我们不仅需要限定一个变量的类型,而且要限定变量的取值在某一个特定的范围内。
let b: "a" | "b" | "c";
如果一个对象是联合类型,那么在类型未确定的情况下,它就只能访问所有类型的共有成员。
class DogImpl implements DogInterface {
run() {}
eat() {}
}
class CatImpl implements CatInterface {
jump() {}
eat() {}
}
enum Master {
Boy,
Girl,
}
function getPet(master: Master) {
let pet = master === Master.Boy ? new DogImpl() : new CatImpl();
pet.eat(); // 如果一个对象是联合类型,那么在类型未确定的情况下,它就只能访问所有类型的共有成员
pet.run(); // error
return pet;
}
这个时候有趣的事情发生了,从名称上看联合类型给人感觉是取所有类型的并集,而实际情况只能访问所有成员的交集。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
}
}
上面的代码如果不去升级是不会有问题的,但如果我们想加一种新的模式,它就会有问题了。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
r: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.r ** 2;
default:
return ((e: never) => {
throw new Error(e);
})(s);
}
}
((e: never) => {
throw new Error(e);
})(s);
这段函数的作用是:检测 s 是不是 never 类型,如果 s 是 never 类型,就说明上面的分支都被覆盖了,这个分支永远不会执行,那么如果 s 不是 never 类型,就说明以前的分支有遗漏。
我们有时候会遇到这样的一种场景,就是从对象中获取一些属性的值然后建立一个集合。
let obj = {
a: 1,
b: 2,
c: 3,
};
function getValues(obj: any, keys: string[]) {
return keys.map((key) => obj[key]);
}
console.log(getValues(obj, ["a", "b"]));
console.log(getValues(obj, ["e", "f"])); // 随意指定不存在的属性,但不报错
随意指定没有的属性,但 ts 编译器并没有报错,所以这个时候我们需要对类型进行约束,这个时候我们就需要用到了索引类型。
下面我们要先了解一下索引类型的几个必要概念
表示泛型变量可以通过继承某个类型获得某些属性
改造上面的代码
let obj = {
a: 1,
b: 2,
c: 3,
};
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map((key) => obj[key]);
}
console.log(getValues(obj, ["a", "b"]));
console.log(getValues(obj, ["e", "f"])); // error 这个时候就报错了
通过映射类型可以把一个旧的类型生成一个新的类型
将一个接口的所有属性映射为只读:
interface objMapping {
a: string;
b: number;
c: boolean;
}
type ReadonlyObj = Readonly<objMapping>;
ReadonlyObj 与 objMapping 成员完全相同,区别是 ReadonlyObj 中的成员属性均为只读
将一个接口的所有属性变成可选的 Partial 映射类型
type PartialObj = Partial<objMapping>;
可以抽取对象子集的 Pick 映射类型:
type PickObj = Pick<objMapping, "a" | "b">;
从源码可以看出 Readonly 是一个可索引类型的泛型接口
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
索引签名为 P in keyof T :
其中 keyof T 就是一个一个索引类型的查询操作符,表示类型 T 所有属性的联合类型
P in :
相当于执行了一个 for in 操作,会把变量 P 依次绑定到 T 的所有属性上
索引签名的返回值就是一个索引访问操作符 : T[P] 这里代表属性 P 所指定的类型
最后再加上 Readonly 就把所有的属性变成了只读,这就是 Readonly 的实现原理
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
Pick 映射类型有两个参数:
第一个参数 T,表示要抽取的目标对象
第二个参数 K,具有一个约束:K 一定要来自 T 所有属性字面量的联合类型,
即映射得到的新类型的属性一定要从 K 中选取
以上三种映射类型官方称为同态类型,意思是只作用于 obj 属性而不会引入新的属性
Record 是非同态类型
type RecordObj = Record<"m" | "n", objMapping>;
第一个参数是预定义的新属性,比如 m,n
第二个参数就是已知类型
映射出的新类型所具有的属性由 Record 的第一个属性指定,而这些属性类型为第二个参数指定的已知类型,这种类型就是一个非同态的类型
Record 映射类型源码:
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
非同态类型本质上会创建新的属性
Readonly, Partial 和 Pick 是同态的,但 Record 不是。 因为 Record 并不需要输入类型来拷贝属性,所以它不属于同态,非同态类型本质上会创建新的属性
映射类型本质上是一种预先定义的泛型接口,通常还会结合索引类型,获取对象的属性和属性值,从而像一个对象映射成我们想要的结构。
条件类型是一种由条件表达式决定的类型
T extends U ? X : Y
意思是如果类型 T 可以赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型,条件类型使类型具有了不唯一性,同样增加了语言的灵活性
当类型 T 为联合类型时:
T 为类型 A 和类型 B 的联合类型,结果类型会变成多个条件类型的联合类型
(A | B) extends U ? X : Y
可以将 A 和 B 进行拆解:
(A extends U ? X : Y) | (B extends U ? X : Y)
这时定义的变量就会被推断为联合类型
type T3 = TypeName<string | string[]>;
可以看到,传入 string | string[]联合类型,被推断为 string|object 的联合类型
利用上边这个特性可以实现对类型的过滤
type Diff<T, U> = T extends U ? never : T;
如果 T 可以被赋值给 U,结果类型为 never 类型,否则为 T 类型
type T4 = Diff<"a" | "b" | "c", "a" | "e">; //通过拆解来分析
T4 的类型被推断为 b 和 c 的联合类型,过滤掉了第二个参数中已经含有类型 a
// Diff<'a', 'a' | 'e'> | Diff<'b', 'a' | 'e'> | Diff<'c', 'a' | 'e'>
// never | "b" | "c"
// "b" | "c"
先判断 a 是否可以被赋值给这个字面量联合类型’a’ | ‘e’,答案是可以的,所以返回 never
继续,因为 b 不可以被赋值给字面量联合类型’a’ | ‘e’,所以返回 b
继续,c 不可以被赋值给’a’ | ‘e’,所以返回 c
最后,never 和 b,c 的联合类型为’b’ | ‘c’
Diff 类型作用:
可以从类型 T 中过滤掉可以被赋值给类型 U 的类型
也可以实现从类型 T 中移除不需要的类型,如 undefined 和 null
定义一个 NotNull,从 T 中过滤掉 undefined 和 null
type NotNull<T> = Diff<T, undefined | null>;
type T5 = NotNull<string | number | undefined | null>;
过滤掉 undefined 和 null,T5 的类型就变成了 string 和 number
上述的 Diff 和 NotNull 类型,是已经在 TS 内置的类库中被实现的内置类型
Diff 的内置类型叫做 Exclude
NotNull 的内置类型叫做 NonNullable
此外,官方还预置了一些条件类型,如:Extract 和 Exclude
Extract 和 Exclude 相反
Exclude 作用是从类型 T 中过滤掉可以赋值给类型 U 的类型
Extract 作用是可以从类型 T 中抽取出可以赋值给 U 的类型
type T6 = Extract<"a" | "b" | "c", "a" | "e">;
type T7 = Exclude<"a" | "b" | "c", "a" | "e">;
T6 抽取了在类型 U 中存在的类型 a
T7 抽取了在类型 U 中不存在的类型 b 和 c
源码:
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
T extends (…args: any) => any:
ReturnType 要求参数 T 可以赋值给一个函数,这个函数有任意的参数,返回值类型也是任意的
由于函数返回值类型不确定,这里使用了 infer 关键字,表示待推断,延迟推断,需要根据实际的情况确定
infer R ? R : any:
如果实际类型是 R,那么结果类型就是 R,否则返回值类型就是 any