TypeScript笔记(二)

1. 泛型 (generic)

文档解释

介绍

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

泛型之Hello World

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是echo命令。

不用泛型的话,这个函数可能是下面这样:

1
2
3
function identity(arg: number): number {
return arg;
}

或者,我们使用any类型来定义函数:

1
2
3
function identity(arg: any): any {
return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。 如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不是值。

1
2
3
function identity<T>(arg: T): T {
return arg;
}

我们给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

1
let output = identity<string>("myString");  // type of output will be 'string'

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍。利用了类型推论 – 即编译器会根据传入的参数自动地帮助我们确定T的类型:

1
let output = identity("myString");  // type of output will be 'string'

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

使用泛型变量

使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

1
2
3
function identity<T>(arg: T): T {
return arg;
}

如果我们想同时打印出arg的长度。 我们很可能会这样做:

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

如果这么做,编译器会报错说我们使用了arg.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有.length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

1
2
3
4
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一节,会介绍如何创建自定义泛型像Array<T>一样。

泛型类型

上一节,我们创建了identity通用函数,可以适用于不同的类型。 在这节,我们研究一下函数本身的类型,以及如何创建泛型接口。

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

1
2
3
4
5
function identity<T>(arg: T): T {
return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn {
<T>(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn = identity;

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如:Dictionary<string>而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

1
2
3
4
5
6
7
8
9
interface GenericIdentityFn<T> {
(arg: T): T;
}

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

注意,我们的示例做了少许改动。 不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。 当我们使用GenericIdentityFn的时候,还得传入一个类型参数来指定泛型类型(这里是:number),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。

除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用(<>)括起泛型类型,跟在类名后面。

1
2
3
4
5
6
7
8
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

1
2
3
4
5
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

我们在那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型约束

你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

1
2
3
4
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含.length属性的接口,使用这个接口和extends关键字还实现约束:

1
2
3
4
5
6
7
8
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

1
loggingIdentity(3);  // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

1
loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象obj上,因此我们需要在这两个类型之间使用约束。

1
2
3
4
5
6
7
8
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,

1
2
3
function create<T>(c: {new(): T; }): T {
return new c();
}

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BeeKeeper {
hasMask: boolean;
}

class ZooKeeper {
nametag: string;
}

class Animal {
numLegs: number;
}

class Bee extends Animal {
keeper: BeeKeeper;
}

class Lion extends Animal {
keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}

createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!

学习解释

指定函数参数类型

泛型可以用于 函数、接口、类、type

如果在使用的时候 无法确定当时的类型,可以采用泛形来定义

  • 单个泛型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const getArray = <T>(times:number,val:T):T[]=>{
    let result:T[] = [];
    for(let i = 0; i<times;i++){
    result.push(val);
    }
    return result;
    }
    getArray(3,3); // 3 => T => number

    const createArr = <T>(times: number, val: T): T[] => {
    const arr = []
    for (let i = 0; i < times; i++) {
    arr.push(val)
    }
    return arr
    // return Array.from({length:times}).fill(val) as T[]
    }
    console.log(createArr(3, 'abc'))
    console.log(createArr(3, 123))
  • 多个泛型

    写辅助函数的函数的时候可以写 多个泛型用于保存值

    值的交换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function swap<T, K>(tuple: [T, K]): [K, T] {
    return [tuple[1], tuple[0]]
    }
    console.log(swap(['a','b']))

    function swap<T,K>(tuple:[T,K]):[K,T]{
    return [tuple[1],tuple[0]]
    }
    const r = swap(['jw',true])


    // IForEach<T> 表示使用接口的时候确定类型
    // <T>():void 在使用这个函数的函数传入类型
    interface IForEach {
    <T>(arr: T[],callback:(val:T)=>void): void
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // type ICallback = <T>(val: T) => void  //  错误写法 泛型的使用需要能正常推到,但是内部的callback没有真正的执行,还是认为arr:T[]
    type IForEach = <T>(arr: T[], callback: ICallback) => void
    const forEach: IForEach = (arr, callback) => {
    for (let i = 0; i < arr.length; i++) {
    // ts 并没有真正的执行内部的callback(arr[i])
    callback(arr[i])
    }
    }

    // 使用forEach才传递的类型,而不是定义接口的时候 传递类型
    // string | number | {}
    forEach(['A', 2, 3,{}], function (val) {
    console.log(val)
    })

函数标注的方式

  • 类型别名

    1
    2
    3
    4
    type TArray = <T, K>(tuple: [T, K]) => [K, T];
    const getArray:TArray = <T, K>(tuple: [T, K]): [K, T] => {
    return [tuple[1], tuple[0]]
    }

    可以使用类型别名,但是类型别名不能被继承和实现。一般联合类型可以使用类型别名来声明

  • 接口

    1
    2
    3
    4
    5
    6
    interface IArray{
    <T,K>(typle:[T,K]):[K,T]
    }
    const getArray:IArray = <T, K>(tuple: [T, K]): [K, T] => {
    return [tuple[1], tuple[0]]
    }

    能使用interface尽量使用interface

泛型接口使用

接口的返回值可能都是统一的

code, data, message

泛型的默认值来解决泛型的值默认情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface ISum<T> { // 这里的T是使用接口的时候传入
<U>(a: T, b: T): U // 这里的U是调用函数的时候传入
}
let sum: ISum<number> = (a:number, b:number) => {
return 3 as any
}

interface APIResponse<T = any> {
error: number,
data: T,
message?: string
}
interface LoginInfo {
username: string,
token: string
}
function login(): APIResponse { // 请求方法
return {
error: 1,
data: {
username: '张三',
token: 'xxxx'
},
message: '成功'
}
}
let r = login()

// 在平时开发中我门想使用联合类型
type IUnion<T = boolean> = T | string | number
type t1 = IUnion
type t2 = IUnion<string[] | number[]>

默认泛型

1
2
3
4
5
interface T2<T=string>{
name:T
}
type T22 = T2;
let name1:T22 = {name:'zf'}

可以指定泛型的默认类型,方便使用

类中的泛型

  • 创建实例时提供类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class MyArray<T>{ // T => number
    arr: T[] = [];
    add(num: T) {
    this.arr.push(num);
    }
    getMaxNum(): T {
    let arr = this.arr
    let max = arr[0];
    for (let i = 1; i < arr.length; i++) {
    let current = arr[i];
    current > max ? max = current : null
    }
    return max;
    }
    }
    let myArr = new MyArray<number>();
    myArr.add(3);
    myArr.add(1);
    myArr.add(2);
    console.log(myArr.getMaxNum());



    class MyList<T extends string | number = number> {
    private arr: T[] = []
    add(val: T) {
    this.arr.push(val)
    }
    getMax(): T {
    let max = this.arr[0];
    for (let i = 1; i < this.arr.length; i++) {
    let cur = this.arr[i];
    cur > max ? (max = cur) : void 0
    }
    return max
    }
    }
    const list = new MyList<string>
    list.add('1')
    list.add('100');
    list.add('200');

    list.getMax(); // 200



  • 校验构造函数类型

    ts只是对类型做校验,对于真正的业务逻辑不关心

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const createClass = <T>(clazz: new(name:string,age:number)=>T):T =>{
    return new clazz(name,age);
    }
    createClass<Person2>(Person2)


    function getObjVal<T extends object, K extends keyof T>(target: T, key: K) {
    return target[key]
    }
    let person = { name: 'jw', age: 30 };
    let animal = { name: 'tom', age: 18, address: 'china' };
    getObjVal(animal, "address");

泛型约束 (extends)

  • 泛型必须包含某些属性

    泛型是用户传递的类型(用户随便传?) 在使用泛型的时候 都要添加约束 泛型约束

    我门在使用泛型的时候 不能直接做运算的 (无法保证泛型的结果 t + t = t?)

    1
    2
    3
    4
    5
    6
    7
    interface IWithLength {
    length:number
    }
    function getLen<T extends IWithLength>(val:T){
    return val.length;
    }
    getLen('hello');
    1
    2
    3
    4
    const sum = <T extends number>(a: T, b: T): T => {
    return (a + b) as T
    }
    let r = sum<number>(1, 2);

    约束当前这个类型T 需要是 string | number 子类型

    1
    2
    3
    4
    5
    6
    function getLen<T extends { length: number }>(val: T) {
    return val.length
    }
    getLen('123')
    getLen({ length: 123 })
    // getLen(123) 约束是某种类型的子类型
  • 返回泛型中指定属性

    1
    2
    3
    const getVal = <T,K extends keyof T>(obj:T,key:K) : T[K]=>{
    return obj[key];
    }

2.类型兼容性

文档解释

介绍

TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
interface Named {
name: string;
}

class Person {
name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

在使用基于名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。

TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。

关于可靠性的注意事项

TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。

开始

TypeScript结构化类型系统的基本规则是,如果x要兼容y,那么y至少具有与x相同的属性。比如:

1
2
3
4
5
6
7
8
interface Named {
name: string;
}

let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;

这里要检查y是否能赋值给x,编译器检查x中的每个属性,看是否能在y中也找到对应属性。 在这个例子中,y必须包含名字是namestring类型成员。y满足条件,因此赋值正确。

检查函数参数时使用相同的规则:

1
2
3
4
function greet(n: Named) {
alert('Hello, ' + n.name);
}
greet(y); // OK

注意,y有个额外的location属性,但这不会引发错误。 只有目标类型(这里是Named)的成员会被一一检查是否兼容。

这个比较过程是递归进行的,检查每个成员及子成员。

比较两个函数

相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:

1
2
3
4
5
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。

第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。

你可能会疑惑为什么允许忽略参数,像例子y = x中那样。 原因是忽略额外的参数在JavaScript里是很常见的。 例如,Array#forEach给回调函数传3个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:

1
2
3
4
5
6
7
let items = [1, 2, 3];

// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));

下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:

1
2
3
4
5
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error because x() lacks a location property

类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum EventType { Mouse, Keyboard }

interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

可选参数及剩余参数

比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。

当一个函数有剩余参数时,它被当做无限个可选参数。

这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded

有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:

1
2
3
4
5
6
7
8
9
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... Invoke callback with 'args' ... */
}

// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));

// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));

函数重载

对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

1
2
3
4
5
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green; //error

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}

class Size {
feet: number;
constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s; //OK
s = a; //OK

类的私有成员

私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

泛型

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,

1
2
3
4
5
6
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;

x = y; // okay, y matches structure of x

上面代码里,xy是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

1
2
3
4
5
6
7
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y; // error, x and y are not compatible

在这里,泛型类型在使用时就好比不是一个泛型类型。

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。

比如,

1
2
3
4
5
6
7
8
9
let identity = function<T>(x: T): T {
// ...
}

let reverse = function<U>(y: U): U {
// ...
}

identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any

高级主题

子类型与赋值

目前为止,我们使用了兼容性,它在语言规范里没有定义。 在TypeScript里,有两种类型的兼容性:子类型与赋值。 它们的不同点在于,赋值扩展了子类型兼容,允许给any赋值或从any取值和允许数字赋值给枚举类型或枚举类型赋值给数字。

语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implementsextends语句也不例外。 更多信息,请参阅TypeScript语言规范.

学习解释

基本数据类型的兼容性

ts兼容性分成两种 子 extends 父 结构来考虑

1
2
3
4
5
6
7
let str: string = "abc"; // 类型层级
let obj!: { toString(): string };

obj = str; // 结构来考虑 extends object extends {}

// 安全性 ts 主要考虑的就是安全, 安全就可以进行复制
// obj.toString
1
2
3
let temp:string | number;
let num!:number;
temp = num;

你要的我有就可以

1
2
3
4
5
let num:{
toString():string
}
let str:string = 'zf';
num = str; // 字符串中具备toString()方法,所以可以进行兼容

接口兼容性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IAnimal {
name: string,
age: number
}
interface IPerson {
name: string,
age: number,
address: string
}
let animal: IAnimal;
let person: IPerson = {
name: 'zf',
age: 11,
address: '回龙观'
};
animal = person;

接口的兼容性,只要满足接口中所需要的类型即可!

函数的兼容性

函数的兼容性主要是比较参数和返回值

  • 参数

    1
    2
    3
    let sum1 = (a: string, b: string) => a + b;
    let sum2 = (a: string) => a;
    sum1 = sum2

    赋值函数的参数要少于等于被赋值的函数,与对象相反,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Func<T> = (item: T, index: number) => void
    function forEach<T>(arr: T[], cb: Func<T>) {
    for (let i = 0; i < arr.length; i++) {
    cb(arr[i], i);
    }
    }
    forEach([1, 2, 3], (item) => {
    console.log(item);
    });
  • 返回值

    1
    2
    3
    4
    5
    6
    type sum1 = () => string | number
    type sum2 = () => string;

    let fn1: sum1;
    let fn2!: sum2;
    fn1 = fn2;
  • 参数和返回值的兼容性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let sum1 = (a: number, b: number): string | number => a + b;
    let sum2 = (a: number): number => a;

    type Sum1 = typeof sum1;
    type Sum2 = typeof sum2;

    type X = Sum2 extends Sum1 ? true : false;

    // 对于函数而言他的兼容性, 少的可以赋予给多的, 参数少的是子类型
    // 返回值要求安全, 返回值要求是子类型.
    const forEach = <T>(
    arr: T[],
    callback: (val: T, key: number) => string | number
    ) => {
    for (let i = 0; i < arr.length; i++) {
    let r = callback(arr[i], i); // 调用函数的时候 会传递多个参数
    }
    };
    forEach(["A", 2, 3, {}], function (val) {
    return "abc";
    });

类的兼容性

1
2
3
4
5
6
7
8
9
class Perent {
name: string = 'zf';
age: number = 11
}
class Parent1 {
name: string = 'zf';
age: number = 11
}
let parent: Perent = new Parent1

这里要注意的是,只要有private或者protected关键字类型就会不一致;但是继承的类可以兼容

1
2
3
4
5
6
class Parent1 {
protected name: string = 'zf';
age: number = 11
}
class Child extends Parent1{}
let child:Parent1 = new Child;

类的兼容性 也是一样 比较的是实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A {
private a = 1;
}
class B {
private a = 1;
}
const b: B = new A(); // 如果类中的属性 有private 或者protected则两个值不能互相复制

// ts 这种叫结构化类型,标称类型

// 希望给基本类型做区分,达到差异化的目的

type withType<T, K> = T & [K];
type BTC = withType<number, "BTC">;
type USDT = withType<number, "USDT">; // 基于内置类型来做构建

const c1 = 100 as BTC;
const c2 = 100 as USDT;
function money(val: BTC) {}
money(c1)

泛型的兼容性

泛型兼容性, 如果生成的结果一致 类型就就兼容

1
2
3
4
5
6
7
interface IT<T>{}
let obj1:IT<string>;
let obj2!:IT<number>;
obj1 = obj2;

type II<T> = { name?: T };
type X1 = II<string> extends II<string> ? true : false; // 生成结构一致即可

枚举的兼容性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum USER1 {
role = 1
}
enum USER2 {
role = 1
}
let user1!:USER1
let user2!:USER2
user1 = user2 // 错误语法
// -------------
enum E1 {
a = 1,
}
enum E2 {
a = 1,
}

let e1!: E1.a;
let e2!: E2.a;

// e2 = e1; //两个枚举之间 不能兼容

不同的枚举类型不兼容

对象的兼容性

对象的兼容性, 多的属性可以赋予给少的

类型层级兼容性,

never -> 字面量 -> 基础类型 -> 包装类型 -> any / unknown

子 extends 父 满足 即可赋值

函数的逆变与协变

函数的参数是逆变的,返回值是协变的 (在非严格模式下函数的参数是双向协变的)

1
2
3
4
5
6
7
8
9
10
11
12
13
class Parent {
address: string = '回龙观'
}
class Child extends Parent {
money: number = 100
}
class Grandsom extends Child {
name: string = '吉姆';
}
type Callback = (person: Child) => Child
function execCallback(cb: Callback) { }
let fn = (person: Parent) => new Grandsom;
execCallback(fn);

通过这个案例可以说明,函数参数可以接收父类,返回值可以返回子类

逆变(在函数参数可以标记儿子传父亲)和协变(可以标记父亲返回儿子)

1
2
3
4
5
6
7
8
9
10
11
class Parent {
car() {}
}
class Child extends Parent {
house() {}
}
class Grandson extends Child {
sleep() {}
}


安全性考虑

内部调用函数的时候 可以传递 Child 和 Grandson. 但是在使用属性时 只能认为最多就是child

函数的返回值, 需要返回子类,因为内部代码在访问属性的时候要保证可以访问到

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn(callback: (ctr: Child) => Child) {
// 我交给回调的有房、有车
let r = callback(new Child());
r.house;
}
fn((child: Parent): Grandson => {
return new Grandson();
});

type Arg<T> = (arg: T) => void;
type Return<T> = (arg: any) => T;
type ArgReturn = Arg<Parent> extends Arg<Child> ? true : false; // 基于函数参数的逆变
type ReturnReturn = Return<Grandson> extends Return<Child> ? true : false; //

逆变带来的问题

(我们写业务的时候 还是要正常开启逆变)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface MyArray<T> {
// concat: (...args: T[]) => T[];
concat(...args: T[]): T[]; // 这种写法不进行逆变检测,所有在描述对象中的方法时全部采用这种方式
}
// parent: (...args: Parent[]) => Parent[];
// child: (...args: Child[]) => Child[];
// 将child 赋予给parent 传父返儿子
let parentArr!: MyArray<Parent>;
let childArr!: MyArray<Child>;

// chilldArr 能不能赋予给 parentArr

// [{car(){}}] = [{car(){},house(){}}]

parentArr = childArr;
// childArr = parentArr;

3.类型保护

通过判断识别所执行的代码块,自动识别变量属性和方法

类型保护 基于js + ts (收窄)

ts很多情况下 需要使用联合类型, 默认情况下只能使用公共的方法,识别类型 (针对某个类型进行处理)

typeof(基础类型) instanceof(类类型) in(接口类型,可辨识类型)

typeof类型保护

1
2
3
4
5
6
7
function double(val: number | string) {
if (typeof val === 'number') {
val
} else {
val
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fn(a: string | number) {
if (typeof a === "string") {
a; // string
} else {
a; // number
}
}
class Cat {
cry() {}
}
class Dog {
eat() {}
}
function getInstance(clazz: { new (...args: any[]): Cat | Dog }) {
return new clazz();
}

instanceof类型保护

1
2
3
4
5
6
7
8
9
10
11
12
13
class Cat { }
class Dog { }

const getInstance = (clazz: { new(): Cat | Dog }) => {
return new clazz();
}
let r = getInstance(Cat);
if(r instanceof Cat){
r
}else{
r
}

1
2
3
4
5
6
const instance = getInstance(Cat);
if (instance instanceof Cat) {
instance.cry;
} else {
instance.eat;
}

in类型保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Fish {
swiming: string,
}
interface Bird {
fly: string,
leg: number
}
function getType(animal: Fish | Bird) {
if ('swiming' in animal) {
animal // Fish
} else {
animal // Bird
}
}

is 语法 ts语法 主要在辅助的方法中用的比较多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Bird {
kind: "鸟";
fly: string;
}
interface Fish {
kind: "鱼";
swim: string;
}
function isBird(val: Bird | Fish): val is Bird {
// ts的返回值类型
// 函数的名字 和返回值是无关的
// true是bird 还是false 是bird
return "fly" in val;
}
function getAimal(val: Bird | Fish) {
// 基于差异化来辨别
if (isBird(val)) {
val;
} else {
val;
}
}

可辨识联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
interface WarningButton {
class: 'warning'
}
interface DangerButton {
class: 'danger'
}
function createButton(button: WarningButton | DangerButton) {
if (button.class == 'warning') {
button // WarningButton
} else {
button // DangerButton
}
}

可辨识类型 通过in来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
nterface Bird {
kind: "鸟";
fly: string;
}
interface Fish {
kind: "鱼";
swim: string;
}

function getAimal(val: Bird | Fish) {
// 基于差异化来辨别
if ("fly" in val) {
val;
} else {
val;
}
if (val.kind == "鸟") {
val.fly;
} else {
val.swim;
}
}

通过各种判断来缩小范围 生命周期 []

1
2
3
4
5
6
7
8
9
function ensureArray<T>(input: T | T[]) {
if (Array.isArray(input)) {
return input;
} else {
return [input];
}
}
let r1 = ensureArray("abc");
let r2 = ensureArray(["abc"]);

函数的嵌套不识别的问题 ? ! if 都有缩小范围的用途 (基于上下文类型的推导,会因为作用域的变化 而产生问题)

! 一定存在的意思 ?取值

1
2
3
4
5
6
7
8
function addType(val?: number) {
// 断言就是自己说的算,出错了自己承担
val = val || 0;
return function (type: string) {
return type + (val as number).toFixed(); // ts 无法识别的时候 需要用断言
};
}
addType(100)("$");

null保护

1
2
3
4
5
6
7
8
const addPrefix = (num?: number) => {
num = num || 1.1;
function prefix(fix: string) {
return fix + num?.toFixed()
}
return prefix('zf');
}
console.log(addPrefix());

这里要注意的是ts无法检测内部函数变量类型

自定义类型保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Fish {
swiming: string,
}
interface Bird {
fly: string,
leg: number
}
function isBird(animal: Fish | Bird):animal is Bird {
return 'swiming' in animal
}
function getAniaml (animal:Fish | Bird){
if(isBird(animal)){
animal
}else{
animal
}
}

完整性保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface ICircle {
kind: 'circle',
r: number
}
interface IRant {
kind: 'rant',
width: number,
height: number
}
interface ISquare {
kind: 'square',
width: number
}
type Area = ICircle | IRant | ISquare
const isAssertion = (obj: never) => { }
const getArea = (obj: Area) => {
switch (obj.kind) {
case 'circle':
return 3.14 * obj.r ** 2
default:
return isAssertion(obj); // 必须实现所有逻辑
}
}

4.交叉类型

交叉类型(Intersection Types)是将多个类型合并为一个类型

联合类型(并集) | 按位或 交叉类型 (交集) & 按位与

交叉类型 (Intersection Types)

1
2
3
4
5
6
7
8
interface Person1 {
handsome: string,
}
interface Person2 {
high: string,
}
type P1P2 = Person1 & Person2;
let p: P1P2 = { handsome: '帅', high: '高' }

举例:我们提供两拨人,一拨人都很帅、另一拨人很高。我们希望找到他们的交叉部分 => 又高又帅的人

1
2
3
4
function mixin<T, K>(a: T, b: K): T & K {
return { ...a, ...b }
}
const x = mixin({ name: 'zf' }, { age: 11 })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface IPerson1 {
name:string,
age:number
}

interface IPerson2 {
name:number
age:number
}
type person = IPerson1 & IPerson2
let name!:never
let person:person = {name,age:11}; // 两个属性之间 string & number的值为never

//--------------------------------

interface Person1 {
handsome:string,
gender:number
meta:{
n:number
}
}
interface Person2 {
high:string,
gender:string
meta:{
n:string
}
}
type Person3 = Person1 & Person2
type IGender = Person3['meta']['n']; // 如果两个类型不相同没有交集 &集后的结果是never

1
2
3
4
5
6
7
8
9
10
11
12
let person:Person1 | Person2 = {
high:'高'
}
// 交叉类型 同时是两个类型的子类型, 最终的结果可以赋予给任何的一个类型
let person:Person1 & Person2 = {
handsome:'帅',
high:'高',
}
// 子类型可以赋予给 父类型 (子类型的结构要包含父类型的结构)

let person1:Person1 =person
let person2:Person2 =person

快速扩展属性

1
2
3
4
5
6
7
let obj = {name:'jw',age:30}
let person:{name:string,age:number,address:string} = obj as typeof obj & {address:string}
function merge<T extends object , K extends object>(o1:T,o2:K){
return {...o1,...o2}
}
let result = merge({name:'abc'},{name:123})
// result.name

交叉类型和联合类型的区别, 平时我门使用的交叉类型场景还是很多的

5.条件类型

条件类型 if / else 三元表达式 (extends 左边 和 右边的关系)

条件类型基本使用

可以使用extends关键字和三元表达式,实现条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Fish {
name1: string
}
interface Water {
name2: string
}
interface Bird {
name3: string
}
interface Sky {
name4: string
}
type Condition<T> = T extends Fish ? Water : Sky;
let con1: Condition<Fish> = { name2: '水' }

子类型 extends 父类型 = true

1
2
3
4
5
6
type StatusCode<T> = T extends 200 | 201 | 204 | 304 ? "success" : "fail";
type IReturnMessage = StatusCode<200>;

type IObj<T> = T extends { name: "jw" } ? "ok" : "no ok";
type IPerson = IObj<{ name: "jw"; age: 30 }>;
type IPerson1 = IObj<{}>;

类型级别 1) 根据结构的角度来分析 2) 从类型角度来进行分析

never 是任何类型的子类型

字面量类型

基础类型

包装的类型

any unknown

1
2
3
type T1 = never extends "str" ? true : false;
type T2 = "str" extends string ? true : false;
type T3 = string extends String ? true : false;

{} object Object {} 和 object 可以看成字面量类型

1
2
3
4
5
6
type Temp1 = {} extends object ? true : false;
type Temp2 = object extends {} ? true : false;
type Temp3 = object extends Object ? true : false;
type Temp4 = Object extends object ? true : false; // 因为从结构角度出发
type Temp5 = Object extends {} ? true : false;
type Temp6 = object extends {} ? true : false;

{} object 可以看成结构, 和类型两部分

1
2
3
4
5
6
7
8
9
type T4 = string extends object ? true : false;

type T5 = string extends any ? true : false;
type T6 = string extends unknown ? true : false;

type T7 = any extends unknown ? true : false;
type T8 = unknown extends any ? true : false;

type T9 = any extends 1 ? true : false; // (条件类型 是有分发机制) 1 + 除了1的部分 true | false

条件类型分发

1
let con2: Condition<Fish|Bird> = { name2: '水' } 

这里会用每一项依次进行分发,最终采用联合类型作为结果,等价于:

1
2
3
type c1 = Condition<Fish>;
type c2 = Condition<Bird>;
type c = c1 | c2
any自带分发的机制

never 如果通过泛型传入,此时只会返回never

1
2
type T10<T> = T extends string ? true : false;
type Temp7 = T10<never>;
联合类型的子类型 是联合类型中的某个类型
1
type T11 = 100 extends 100 | 200 ? true : false;

通过条件类型 来进行类型的区分,条件语句也可以实现约束的效果

1
2
3
4
5
6
7
8
9
10
11
12
interface Fish {
name: "鱼";
}
interface Bird {
name: "鸟";
}
interface Water {
name: "水";
}
interface Sky {
name: "天";
}
T & {} 就可以解决分发问题
1
type GetType<T extends Fish | Bird> = T & {} extends Fish ? Water : Sky;

分发导致的问题: 什么时候会有分发

1.联合类型通过泛型传递

2.而且比较(extends)的时候会产生分发

3.类型需要是裸类型 (裸类型就是泛型 就自己没有和别人搭配)

1
2
type A1 = GetType<Fish | Bird>;
type NoDistribute<T> = T & {};

分发机制有的场景需要,有的场景需要禁用, 不能一概而论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type UnionAssets<T, K> = NoDistribute<T> extends K ? true : false;
type U1 = UnionAssets<1 | 2, 1 | 2 | 3>;
type U2 = UnionAssets<1 | 2 | 3, 1 | 2>;

// 判断两个类型是否完全一致 ? 1|2 1|2

type isEqual<T, K, S, F> = NoDistribute<T> extends K
? NoDistribute<K> extends T
? S
: F
: F;
type A2 = isEqual<1 | 2, 1 | 2, true, false>;

type FormatVal<T> = T extends string
? string
: T extends number
? number
: never;

映射关系 可以考虑用泛型,参数个数不一致,类型和入参数无法,考虑重载

1
2
3
4
function sum<T extends string | number>(a: T, b: T): FormatVal<T> {
return a + (b as any);
}
let r = sum(1, 2);

6.内置类型

Exclude排除类型

1
2
type Exclude<T, U> = T extends U ? never : T;
type MyExclude = Exclude<'1' | '2' | '3', '1' | '2'> // 3

Extract抽取类型

1
2
type Extract<T, U> = T extends U ? T : never;
type MyExtract = Extract<'1' | '2' | '3', '1' | '2'> // 12

NoNullable 非空检测

1
2
type NonNullable<T> = T extends null | undefined ? never : T
type MyNone = NonNullable<'a' | null | undefined> // a

ReturnType返回值的类型

1
2
3
4
5
function getUser(a: number, b: number) {
return { name: 'zf', age: 10 }
}
type ReturnType<T> = T extends (...args: any) => infer R ? R : never
type MyReturn = ReturnType<typeof getUser> // type MyReturn = {name: string;age: number;}

Parameters 返回参数的类型

1
2
type Parameters<T> = T extends (...args: infer R) => any ? R : any;
type MyParams = Parameters<typeof getUser>; // type MyParams = [a: number, b: number]

ConstructorParameters返回构造函数的参数类型

1
2
3
4
5
class Person {
constructor(name: string, age: number) { }
}
type ConstructorParameters<T> = T extends { new(...args: infer R): any } ? R : never
type MyConstructor = ConstructorParameters<typeof Person> // type MyConstructor = [name: string, age: number]

InstanceType 返回实例的类型

1
2
type InstanceType<T> = T extends { new(...args: any): infer R } ? R : any
type MyInstance = InstanceType<typeof Person>

infer实践

将数组类型转化为联合类型

1
2
type ElementOf<T> = T extends Array<infer E> ? E : never;
type TupleToUnion = ElementOf<[string, number, boolean]>;

将两个函数的参数转化为交叉类型

1
2
3
4
type T1 = { name: string };
type T2 = { age: number };
type ToIntersection<T> = T extends ([(x: infer U) => any, (x: infer U) => any]) ? U : never;
type t3 = ToIntersection<[(x:T1)=>any,(x:T2)=>any]>

表示要把T1T2赋予给x,那么x的值就是T1T2的交集。(参数是逆变的可以传父类)

TS的类型:TS主要是为了代码的安全性来考虑。所以所有的兼容性问题都要从安全性来考虑!

Partial转化可选属性

1
2
3
4
5
6
7
8
9
10
interface Company {
num: number
}
interface Person {
name: string,
age: string,
company: Company
}
// type Partial<T> = { [K in keyof T]?: T[K] }; 实现原理
type PartialPerson = Partial<Person>;

遍历所有的属性将属性设置为可选属性,但是无法实现深度转化!

1
2
3
4
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
type DeepPartialPerson = DeepPartial<Person>;

我们可以实现深度转化,如果值是对象继续深度转化。

Required转化必填属性

1
2
3
4
5
6
7
8
9
10
11
interface Company {
num: number
}
interface Person {
name: string,
age: string,
company: Company
}
type PartialPerson = Partial<Person>;
type Required<T> = {[K in keyof T]-?:T[K]}
type RequiredPerson = Required<PartialPerson>

将所有的属性转化成必填属性

Readonly转化仅读属性

1
2
type Readonly<T> = { readonly [K in keyof T]: T[K] }
type RequiredPerson = Readonly<Person>

将所有属性变为仅读状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type IPerson = {
name: string;
age: string;
company: string;
};

type Readonly<T> = {
+readonly [K in keyof T]: T[K];
};
type ReadonlyRes = Readonly<IPerson>;

// 不能修改就是readonly , 却掉readonly
type Mutate<T> = {
-readonly [K in keyof T]: T[K];
};
type MutateRes = Mutate<ReadonlyRes>;

// -? +? -readonly +readonly

Pick挑选所需的属性

1
2
type Pick<T, U extends keyof T> = { [P in U]: T[P] }
type PickPerson = Pick<Person, 'name' | 'age'>

在已有类型中挑选所需属性

Omit忽略属性

1
2
3
4
5
6
7
let person = {
name: 'zhufeng',
age: 11,
address: '回龙观'
}
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
type OmitAddress = Omit<typeof person, 'address'>

忽略person中的address属性 (先排除掉不需要的key,在通过key选出需要的属性)

Omit Pick 对象来操作的

1
2
3
4
5
6
7
8
9
10
11
12
13
type IPerson = {
name: string;
age: string;
company: string;
};

type Pick<T, K extends keyof T> = {
[key in K]: T[key];
};
type PickRes = Pick<IPerson, "company" | "age">;

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type OmitRes = Omit<IPerson, "name" | "age">;

Pick和Omit用的比较多

Record记录类型

1
2
type Record<K extends keyof any, T> = { [P in K]  : T }
let person: Record<string, any> = { name: 'zf', age: 11 };

实现map方法,我们经常用record类型表示映射类型

1
2
3
4
5
6
7
8
9
10
function map<T extends keyof any, K, U>(obj: Record<T, K>, callback: (item: K, key: T) => U) {
let result = {} as Record<T, U>
for (let key in obj) {
result[key] = callback(obj[key], key)
}
return result
}
const r = map({ name: 'zf', age: 11 }, (item, key) => {
return item
});

推导出类型 (根据泛型的位置 来推导具体的类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
function map<T extends keyof any, K, R>(
obj: Record<T, K>,
callback: (value: K, key: T) => R
) {
let result = {} as Record<T, R>;
for (let key in obj) {
result[key] = callback(obj[key], key);
}
return result;
}
map({ name: "jw", age: 30 }, (value, key) => {
return true;
});

infer inference 手动类型推导

1
2
3
function getPerson(a: string, b: number) {
return { name: "jw", age: 30 };
}

类型推断 , 因为infer 需要extends 关键字, 所以必须构建一个条件

1
2
3
4
5
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: never;

infer 关键字需要基于extends

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type FnReturnType = ReturnType<typeof getPerson>;
// type Parameters<T extends (...args: any[]) => any> = T extends (
// ...args: infer P
// ) => any
// ? P
// : false;
type FnParamaters = Parameters<typeof getPerson>;

class Person {
constructor(a: string, b: string) {}
}
// type ConstructorParameters<T extends { new (...args: any[]): any }> =
// T extends { new (...args: infer P): any } ? P : any;

type ClassConstructorParameters = ConstructorParameters<typeof Person>;

// type InstanceType<T extends { new (...args: any[]): any }> = T extends {
// new (...args: any[]): infer R;
// }
// ? R
// : any;
type ClassInstanceType = InstanceType<typeof Person>;

如何讲一个元组转化成联合和类型

1
2
3
4
5
type ITumple = [string, number, boolean]; // string | number | boolean
// type TumpleToUniom = ITumple[number];

type ElementOf<T extends any[]> = T extends Array<infer R> ? R : never;
type TumpleToUniom = ElementOf<ITumple>;

对元组进行参数移动, 将元组头尾进行交换 . infer 要配合 extends

1
2
3
4
5
6
7
8
9
10
type SwapHeadTail<T extends any[]> = T extends [
infer Head,
...infer Body,
infer Tail
]
? [Tail, ...Body, Head]
: never;

type Res = SwapHeadTail<["jw", 1, 2, 3, 4, 5, 30]>;

infer 可以递归推断

1
2
3
4
5
6
7
8
9
10
11
12
// function getVal(): Promise<Promise<200>> {
// return new Promise((resolve, reject) => {
// resolve(
// new Promise((resolve, reject) => {
// resolve(200);
// }) as Promise<200>
// );
// });
// }

type PromiseVal<T> = T extends Promise<infer R> ? PromiseVal<R> : T;
type PromiseReturnVal = PromiseVal<Promise<Promise<200>>>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 class Cat {
constructor(public name: string) {}
}
class Dog {
constructor(public name: string) {}
}
function createInstance<T extends { new (...args: any[]): any }>(
clazz: T,
name: string
): InstanceType<T> {
let r = new clazz(name);
return r;
}
const instance = createInstance(Dog, "tom");

7.循环类型

Extract Exclude , NonNullable…

除了基于条件类型之外,我门还有基于对象类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Person1 {
handsome: string;
}
interface Person2 {
high: string;
}

// for(let key in Object.keys(T) ) { }

type IKeys1 = keyof any; // string | number | symbol 使用场景比较多
type IKeys2 = keyof unknown; // never
type Compute<T extends object> = {
[xxxxxxxxx in keyof T]: T[xxxxxxxxx];
};
type Person3 = Compute<Person1 & Person2>;

// partial required pick omit.... 内置类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
interface IPerson {
name: string;
age: number;
}
interface ICompany {
name: string;
age: number;
address: string;
person: IPerson;
}

type Partial<T> = {
[key in keyof T]?: T[key];
};
type DeepPartial<T> = {
// 递归的深度可选
[key in keyof T]?: T[key] extends object ? DeepPartial<T[key]> : T[key];
};
type PartialRes1 = DeepPartial<ICompany>;

type Required<T> = {
[key in keyof T]-?: T[key];
};
type DeepRequired<T> = {
// 递归的深度可选
[key in keyof T]-?: T[key] & {} extends object
? DeepRequired<T[key]>
: T[key];
};
type PartialRes2 = DeepRequired<PartialRes1>;

let company: PartialRes2 = {
name: "string",
age: 10,
address: "string",
person: {
name: "jw",
age: 30,
},
};