0%

作用

infer这个词的含义即 推断,实际作用可以用四个字概括:类型推导。它会在类型未推导时进行占位,等到真正推导成功后,它能准确地返回正确的类型。

在这个条件语句 T extends (...args: infer P) => any ? P : T 中,infer P 表示待推断的函数参数。

整句含义为:如果 T 能赋值给 (...args: infer P) => any,则结果是 (...args: infer P) => any 类型中的参数 P,否则返回为 T

1
2
3
4
5
6
7
8
9
interface User {
name: string;
age: number;
}

type Func = (user: User) => void;

type Param = ParamType<Func>; // Param = User
type AA = ParamType<string>; // string

infer这个关键字在各种高级类型实现中出现频率很高,大部分情况下会与extendskeyof等关键字一起使用。

注意点

infer只能在 extends 条件语句中使用,声明变量只能在true分支中使用

比如我想实现上文中ParamType类型,他接受一个函数类型,然后返回函数参数的类型。

用如下方式实现:

1
2
type ParameType<T extends (...args: infer R) => any> = R
// error: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.

大意就是infer只能在extends条件语句中使用,在extends详解中我们提到extends关键字的使用场景大概有以下几种:接口继承、类型约束以及条件类型。在上述ParameType类型实现中,很明显这是属于类型约束的用法,想要实现该类型需要使用条件类型。

1
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
  • T extends (...args: any) => infer P:如果不看infer R,这段代码实际表示: T是不是一个函数类型。
  • (...args: any) => infer P:这段代码实际表示一个函数类型,把它的参数使用args来表示,把它的返回类型用P来进行占位。
  • 如果T满足是一个函数类型,那么我们返回其函数的返回类型,也就是P;如果不是一个函数类型,就返回never

此外,要注意infer声明的变量只能在true分支中使用

对使用了函数重载的函数进行类型推断

函数重载或⽅法重载是使⽤相同名称和不同参数数量或类型创建多个⽅法的⼀种能⼒。一些 JavaScript 函数在调用的时候可以传入不同数量和类型的参数。举个例子。你可以写一个函数,返回一个日期类型 Date,这个函数接收一个时间戳(一个参数)或者一个 月/日/年 的格式 (三个参数)。在 TypeScript中,我们可以通过写重载签名 (overlaod signatures) 说明一个函数的不同调用方法。 我们需要写一些函数签名 (通常两个或者更多),然后再写函数体的内容:

1
2
3
4
5
6
7
8
9
10
11
12
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);

对使用了函数重载的函数进行类型推断时,以最后一个签名为准,因为一般这个签名是用来处理所有情况的签名。

1
type a = Parameters<typeof makeDate>  //type a = [m: number, d: number, y: number]

infer的位置会影响到推断的结果

这涉及到协变与逆变,具体的区别将在之后的文章中进行讲解,这里只需要知道:协变或逆变与 infer 参数位置有关。在 TypeScript 中,对象、类、数组和函数的返回值类型都是协变关系,而函数的参数类型是逆变关系,所以 infer 位置如果在函数参数上,就会遵循逆变原则。

  • infer在协变的位置上时,同一类型变量的多个候选类型将会被推断为联合类型,
  • infer在逆变的位置上时,同一类型变量的多个候选类型将会被推断为交叉类型。

看例子:

1
2
3
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>; // string
type T11 = Foo<{ a: string, b: number }>; // string | number

按照上文的规则,这应该是属于协变,因此T11结果是string | number

1
2
3
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number

同样地,x这里既有可能是string,也可能是number,但最终却被推断为交叉类型。这就是因为infer所处的是逆变的位置,即这里是在推断函数的参数类型,导致最终推导为交叉类型。

类型体操实战

高质量的类型可以提高项目的可维护性并避免一些潜在的漏洞。【type-challenges】旨在让你更好的了解 TS 的类型系统,编写你自己的类型工具,或者只是单纯的享受挑战的乐趣!

type-challenges】中有各种有关类型操作的小挑战,接下来我将挑选其中与infer有关的一些挑战。

First of Array

要求:实现一个通用First<T>,它接受一个数组T并返回它的第一个元素的类型。

实现:

1
type First<T extends any[]> = T extends [infer L, ...infer R] ? L : never

利用了infer声明了LR进行占位,其中:

  • infer R: 表示数组第一个元素的占位。
  • ...infer L: 表示数组剩余元素的占位。
  • 通过extends判断进入true分支时,返回类型L,否则返回never

当然,上述实现方式是通过占位实现的,也可以通过索引的方式实现。

1
type First<T extends any[]> = T extends [] ? never : T[0]

Capitalize

要求:实现 Capitalize<T> 它将字符串的第一个字母转换为大写,其余字母保持原样。

1
type capitalized = Capitalize<'hello world'> // expected to be 'Hello world'

实现:

1
type Capitalize<S extends string> = S extends `${infer L}${infer R}` ? `${Uppercase<L>}${R}`: S

既然有首字母大写,那么相应的首字母小写Uncapatilize的实现也类似:

1
type UnCapitalize<S extends string> = S extends `${infer L}${infer R}` ? `${Lowercase<L>}${R}`: S

无论首字母大写还是首字母小写,核心实现还是用infer L去占位,然后对其调用Uppercase或者Lowercase

Tuple to Union

要求:

实现泛型TupleToUnion<T>,返回元组所有值的类型组成的联合类型

1
2
3
type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

实现:

1
type TupleToUnion<T extends any[]> = T[number]

T[number]它会自动迭代元组的数字型索引,然后将所以元素组合成一个联合类型

这种解法应该是比较简单直接的,T[number]的使用比较巧妙,但如果是第一次动手实现这样的类型,比较难想到这种解法。

如果想要用infer实现的话,应该如何操作呢?

1
type TupleToUnion<T extends any[]> = T extends [infer L, ...infer R] ? L | TupleToUnion<R> : never

L | TupleToUnion<args>:L表示每一次迭代中的第一个元素,它的迭代过程可以用下面伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第一次迭代
const L = '1'
const R = ['2', '3']
const result = '1' | TupleToUnion<args>

// 第二次迭代
const L = '2'
const R = ['3']
const result = '1' | '2' | TupleToUnion<args>

// 第三次迭代
const L = '3'
const R = ['']
const result = '1' | '2' | '3'

说白了就是递归的思想,想通了也不难。

深入理解TypeScritp中看到一种解法,也很巧妙:

1
type TupleToUnion<T extends any[]> = T extends Array<infer R> ? R : never

该实现的前提是:tuple 类型在一定条件下,是可以赋值给数组类型

1
2
3
4
5
type TTuple = [string, number];
type TArray = Array<string | number>;

type Res = TTuple extends TArray ? true : false; // true
type ResO = TArray extends TTuple ? true : false; // false

那么,之后再利用infer类型推导的功能,T extends Array<infer R>进入true分支,就很容易得到想要的结果了。

Union to Intersection

要求:将联合类型转换为交叉类型

1
type I = Union2Intersection<'foo' | 42 | true> // expected to be 'foo' & 42 & true

这个挑战的标签是hard, 还是很有挑战性的。主要涉及到上述注意点中的第三点,

即:infer在逆变的位置上时,同一类型变量的多个候选类型将会被推断为交叉类型。

直接给出stackoverflow上的解答:

1
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

具体实现可以分为以下几个步骤:

  • 利用extends分配条件类型语句将联合类型中的每一个处理成(x: U) => any这样的函数类型

  • 然后利用当infer在逆变的位置上时,同一类型变量的多个候选类型将会被推断为交叉类型,得到想要的结果。

    其中,逆变的过程类似如下:

    1
    2
    3
    4
    5
    6
    type T1 = { name: string };
    type T2 = { age: number };

    type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
    // 处在逆变位置时,推导出来的为交叉类型
    type T21 = Bar<{ a: (x: T1) => void; b: (x: T2) => void }>; // T1 & T2

    总结

  1. 作用:类型推导,在类型未推导时进行占位,等到真正推导成功后,它能准确地返回正确的类型

  2. 注意点:

  • infer只能在 extends 条件语句中使用,声明变量只能在true分支中使用
  • 对使用了函数重载的函数进行类型推断时,以最后一个签名为准,因为一般这个签名是用来处理所有情况的签名。
  • infer在协变的位置上时,同一类型变量的多个候选类型将会被推断为联合类型;当infer在逆变的位置上时,同一类型变量的多个候选类型将会被推断为交叉类型。

更多

TypeScript类型操作中的关键字详解(一):keyof & in

TypeScript类型操作中的关键字详解(二):extends

参考

精读《Typescript infer 关键字》


Type inference in conditional types

type-challenges

使用

在各种类型操作中,少不了extends关键字的身影,它主要有以下几个作用: 接口继承 类型约束以及条件类型

接口继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
name: string;
age: number;
}

interface Player extends Person {
item: 'ball' | 'swing';
}
//接口继承后
// interface Person {
// name: string;
// age: number;
// item: 'ball' | 'swing';
// }

类型约束

通常和泛型一起使用,那么具体应该如何使用呢?

1
2
3
4
5
6
7
interface Dog {
bark: () => void
}

function dogBark<T extends Dog>(arg: T) {
arg.bark()
}

我们定义类型Dog,它 有一个不返回任何值的bark方法,使用extends关键字进行泛型约束,传入dogBark方法的值必须有bark方法,简单的说extends关键字在这里的作用:作为一个守门员,只让会狗叫的进,管你是不是🐕,只要会狗叫,就可以进;如果不会,请出门右转。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let dogA = {
weight: 12,
age: 4
}

let dogB ={
weight: 12,
age: 4,
bark: () => console.log('dogB is barking')
}

dogBark(dogA)
// error !!!
// Argument of type '{ weight: number; age: number; }' is not assignable to parameter of type 'Dog'.
// Property 'bark' is missing in type '{ weight: number; age: number; }' but required in type 'Dog'.

dogBark(dogB) // success: "dogB is barking"

在使用extends关键字实现一些类型时,可能会用到如下代码:

1
P extends keyof T

表示P的类型是keyof T返回的字面量联合类型,也就是说P原本没限制,是any,限制之后类型变成了keyof T返回的字面量联合类型。

类似的有使用T extends keyof any对对象类型的属性进行约束,keyof any返回的是string | number | symbol,即这也是属性字段的取值范围。

再来看这样一个例子

1
2
3
4
5
6
interface Person {
name: string;
age: number;
}

type NameOf<T> = T['name'] // error: Type '"name"' cannot be used to index type 'T'

NameOf类型的作用是取得传入类型Tname属性的值的类型,但这里却报错了。因为传入的泛型T不一定有属性name, 传入的可能是一个没有name属性的对象,也可能是一个字面量类型,访问T可能没有的属性是不安全的,因此会报错,要解决这个问题就需要对泛型T进行约束,确保其一定具有name这个属性。

1
2
3
4
5
6
7
interface Person {
name: string;
age: number;
}

type NameOf<T extends {'name': unknown}> = T['name'] // success!
type personName = NameOf<Person> //string

条件类型 (Conditional Types )

常见表现形式为:

1
T extends U ? 'Y' : 'N'

可以这样理解:TU的子类型,那么返回结果是'Y', 否则是'N'. 类似JS中的三元表达式,其工作原理是类似的,例如:

1
2
3
4
type res1 = true extends boolean ? true : false                  // true
type res2 = 'name' extends 'name'|'age' ? true : false // true
type res3 = [1, 2, 3] extends { length: number; } ? true : false // true
type res4 = [1, 2, 3] extends Array<number> ? true : false // true

要注意:

  • extends在条件类型中的作用和类型约束中的作用不一样
  • 条件类型只支持在type中使用

此外,**extends作为条件类型时也是可以嵌套的**,就像if语句一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";

type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"



再来看如下代码:

1
2
3
type A1 = P<'x' | 'y'> extends 'x' ? string : number; //  type A1 = number
type P<T> = T extends 'x' ? string : number;
type A2 = P<'x' | 'y'> // ? type A2 = string | number

A2结果为什么不是number呢?实际发生的操作类似如下:

1
2
3
4
type A2 = P<'x' | 'y'> 
type A2 = P<'x'> | P<'y'>
type A2 = ('x' extends 'x' ? string : number) | ('y' extends 'x' ? string : number)
type A2 = string | number

这叫分配条件类型(Distributive Conditional Types

T为泛型时,且传入该泛型的是一个联合类型,那么该联合类型中的每一个类型都要进行上述操作,最终返回上述操作结果组成的新联合类型。换句话说,这里的分配是指将上述提到的”三元表达式”操作应用于联合类型中的每个成员。

要注意的是:

1. extends关键字左侧的是一个泛型,且传入泛型的必须是联合类型,其他类型如交叉类型是没有分配效果的。

如果左侧不是泛型,直接传入一个联合类型,是没有分配效果的,只是一个简单的条件判断。

1
2
3
4
  type A1 = 'x' extends 'x' ? string : number; // string

type A2 = 'x' | 'y' extends 'x' ? string : number; // number
// 如果分配生效的话,结果应该是 string | number

2. 分配操作只有在检查的类型是naked type parameter时才生效。

那么是什么是naked type parameter呢?直接翻译过来怪怪的,参数是的?
我的理解是没有对传进来的泛型参数进行一些额外操作,那么就符合naked type parameter的要求。

看一下以下的例子,更容易理解。这也是stackoverflow上一个高赞回答的例子

1
2
3
4
5
6
type NakedUsage<T> = T extends boolean ? "YES" : "NO"
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO"; // wrapped in a tuple,

type Distributed = NakedUsage<number | boolean > // = NakedUsage<number> | NakedUsage<boolean> = "NO" | "YES"
type NotDistributed = WrappedUsage<number | boolean > // "NO"
type NotDistributed2 = WrappedUsage<boolean > // "YES"

其中,WrappedUsage对传入的泛型参数进行了操作,不属于naked type parameter,因此不会进行分配操作。

类型操作实战

Pick & Record

extends类型约束特性相关的工具类型有PickRecord

Pick

Pick表示从一个类型中选取指定的几个字段组合成一个新的类型,用法如下:

1
2
3
4
5
6
7
8
9
type Person = {
name: string;
age: number;
address: string;
sex: number;
}

type PickResult = Pick<Person, 'name' | 'address'>
// { name: string; address: string; }

实现方式

1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

首先进行了类型限定,K一定是T的子集,然后用in遍历K中的每个属性, T[P]是属性对应的值。

Record

Record<K, T>用来将K的每一个键(k)指定为T类型,这样由多个k/T组合成了一个新的类型,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
type keys = 'Cat'|'Dot'
type Animal = {
name: string;
age: number;
}

type RecordResult = Record<keys, Animal>
// result:
// type RecordResult = {
// Cat: Animal;
// Dot: Animal;
// }

实现方式

1
2
3
type Record<K extends keyof any, T> = {
[P in K]: T
}

keyof any是什么鬼?鼠标放上去看看就知道了

因此,keyof anystring | number | symbol,先对键的取值范围进行了限定,只能是这三者中的一个。

Exclude & Extract & NonNullable

extends条件类型特性相关的工具类型又有哪些呢?

先看着两个:ExcludeExtract

Exclude<T, U>: 排除T中属于U的部分

image.png
Extract<T, U>: 提取T中属于U的部分,即二者交集

image.png

使用方法

1
2
type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>
//type ExcludeResult = "name" | "age"
1
2
type ExcludeResult = Extract<'name'|'age'|'sex', 'sex'|'address'>
//type ExcludeResult = "sex"

实现方式

1
2
3
type Exclude<T, U> = T extends U ? never : T

type extract<T, U> = T extends U ? T : never

实现思路不再赘述,见前文extends分配条件类型的原理

NonNullable工具类型可以从目标类型中排除nullundefined,和Exclude相比,它将U限定的更具体。

实现也很简单:

1
2
3
4
5
6
type A = null | undefined | 'dog' | Function

// type nonNullable<T> = Exclude<T , undefined | null>
type nonNullable<T> = T extends null | undefined ? never : T

type res = nonNullable<A> // type res = Function | "dog"

Omit

根据已经实现的Exclude类型,可以实现Omit类型,Omit<T, K>:删除T中指定的字段,用法如下:

1
2
3
4
5
6
7
8
type Person = {
name?: string;
age: number;
address: string;
}

type OmitResult = Omit<Person, 'address'>
// 结果:{ name?: string; age: number; }

实现方式

1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

首先,删除指定字段,字段类型限定在 string | symbol number中,然后用ExcludeT的属性所组成的字面量联合类型中移除指定字段,形成新的联合类型;最后利用Pick选取指定字段生成新的类型

AppendToObject

AppendToObject的作用是向指定对象中添加一个属性, 同时指定属性值的类型。如果该属性字段之前就存在,新增的字段会被忽略。

注:该类型并不是内置工具类型

使用方式

1
2
3
4
5
type Test = { id: '1' }
// expected to be { id: '1', value: 4 }
type Result = AppendToObject<Test, 'value', 4>
// 结果:{ id: number; name: string; }
type result = AppendToObject<{ id: number; }, 'name', string>

实现方式

1
2
3
type AppendToObject<T, K extends keyof any, V> = {
[P in keyof T | K]: P extends keyof T ? T[P] : V
}

首先, 需要遍历的所有属性包含T中的属性字段和新增的字段K,即keyof T | K,然后使用in关键字进行遍历操作,对遍历到的每个属性字段使用extends进行判断,如果遍历到的字段P是原本T中就存在的属性字段,判断为true,返回T[p];否则为false,说明该属性字段之前并不存在,返回新增字段对应的类型V

Merge

Merge将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。

使用方式

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

type coo = {
age: number;
sex: string
}

type Result = Merge<foo,coo>; // expected to be {name: string, age: number, sex: string}

实现方式

1
2
3
type Merge<F, S> = {
[P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never
}

这里使用了两次extends, 写成下方这种形式可能更清楚一些:

1
2
3
4
5
6
type Merge<F, S> = {
[P in keyof F | keyof S]:
P extends keyof S
? S[P]
: (P extends keyof F ? F[P] : never)
}

AppendToObject一样,首先用in关键字遍历所有的属性字段(keyof F | keyof S), 在此过程中对每一字段进行相应判断,因为S中的对应的字段会对F中相同字段进行覆盖,因此先判断该字段是否属于S,然后再判断该字段是否属于P

参考

ts handbook

TypeScript 的 extends 条件类型

type challenges

TypeScript类型操作中typeofin经常放在一起使用,使用频率也很高,因此将这两个关键字放在一起介绍。

keyof

使用

keyof操作符接受一个对象类型作为参数,返回该对象属性名组成的字面量联合类型

1
2
type Dog = { name: string; age: number;  };
type D = keyof Dog; //type D = "name" | "age"

在一些高级类型中经常会用到keyof any, 这又是什么鬼?鼠标放上去看看就知道了
image.png

可以看到keyof any 返回的是一个联合类型:string | number | symbol,结合前文说到keyof是为了取得对象的key值组成的联合类型,那么key值有可能是什么类型呢?自然就是string | number | symbol

该关键字一般会和extends关键字结合使用,对对象属性的类型做限定,比如K extends keyof any就代表K的类型一定是keyof any所返回的联合类型的子类,如果输入不符合限定,那么自然也就不能作为对象的属性,类型系统就会报错。

因此,keyof any 表示了对象key值可能的取值类型。这一点在本文之后的一些类型实现中也会用到。

注意点

遇到索引签名时,typeof会直接返回其类型

1
2
3
4
5
6
7
8
type Dog = {  [y:number]: number  };
type dog = keyof Dog; //type dog = number

type Doggy = { [y:string]: boolean };
type doggy = keyof Doggy; //type doggy = string | number

type Doggy = { [y:string]: unknown, [x:number]: boolean};
type doggy = keyof Doggy; //type doggy = string | number

可以看到索引类型为number时,keyof 返回的类型是string | number, 这是因为JavaScript的对象属性会默认转换为字符串。

in

in的右侧一般会跟一个联合类型,使用in操作符可以对该联合类型进行迭代。
其作用类似JS中的for...in或者for...of

1
2
3
4
5
6
7
8
9
10
type Animals = 'pig' | 'cat' | 'dog'

type animals = {
[key in Animals]: string
}
// type animals = {
// pig: string; //第一次迭代
// cat: string; //第二次迭代
// dog: string; //第三次迭代
// }

类型操作实战

Partial & Required

Partial:将某个类型里的属性全部变为可选项

思路是通过泛型传入待处理类型,先用keyof取到所给类型所有属性组成的字面量联合类型,然后使用in进行遍历,同时结合 ?操作符,将每个属性变成可选的

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P]
}

[P in keyof T]这段代码表示遍历T中的每一个属性,那么T[P]就是每个属性所对应的值,可以简单理解为前者取的是键key,后者取的是值value

Required:和Partial的作用相反,是为了将某个类型里的属性全部变为必选的

1
2
3
4
5
6
7
8
9
interface Props {
a?: number;
b?: string;
}

const obj: Props = { a: 5 }; // b是可选的,因此缺少这个属性也可以

const obj2: Required<Props> = { a: 5 }; // 通过Required将属性变为必选的,等号右边对象缺少这个属性,因此赋值失败
//Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

实现思路和前面相似

1
2
3
type Partial<T> = {
[P in keyof T]-?: T[P]
}

上文对应的-?代表着去掉可选,与之对应的还有+?,两者正好相反

promise解决了什么问题?

主要是回调嵌套和控制反转

回调嵌套

嵌套和缩进只是格式层面的问题,深层次问题是代码难以复用、堆栈信息断开、引用外层变量

  • 代码难以复用

    回调中引用了外层变量,提取出来后需要进行相应修改

  • 堆栈信息断开

    异步回调函数执行时将回调函数放入任务队列中,代码继续执行,直到主线程完成,然后才会从任务队列中选择已经完成的任务放入执行栈中,如果回调报错,无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。

  • 借助外层变量

    多个异步计算同时进行,由于无法预期完成顺序,必须借助外层作用域的变量,可能被其它同一作用域的函数访问并且修改,容易造成误操作。

控制反转

使用第三方api,回调函数的执行次数、是否执行、执行时机都取决于第三方库的实现

  1. 回调函数执行多次 ===> promise只能resolve一次
  2. 回调函数没有执行 ===> 使用Promise.race判断
  3. 回调函数有时同步执行有时异步执行 ===> promise总是异步的

async await的问题

场景一:组合多个promsie

await关键字是串行的,想要同时获取多个promise的结果,需要用promise的api进行处理,比如promise.all

场景二:存储promsie的值

async/await 是语法,不是值,因此它不能被存储和传递。而 promise 对象,可以存储在内存里,可以作为参数在函数中传递。

image.png

建立url到promise映射,通过async/await语法隐藏了promise对象,顶多建立url到result的缓存,但是当页面上发出get请求,结果未抵达,但是又触发了多个相同请求的话就无法命中result缓存,如果缓存的是promise的对象,就可以将同一个promise返回,利用promise对象可以多次调用then方法的特性,让所有相同的get请求获得同一个异步请求结果。

Promise.all

所有的 promsie 都resolve时, 返回存储结果的数组,如果其中一个promise reject,那么会忽略剩余promise 的结果,哪怕其他promise都resolved,剩下的最后一个promise rejected, 其error会变成整个 Promise.all的结果, 已经resolved 的结果会被忽略。

应用场景:

合并请求结果

具体描述:一个页面,有多个请求,我们需求所有的请求都返回数据后再一起处理渲染

不需要为每个请求都设置loading状态,从请求开始到请求结束,只需要设置一个loading状态

可能的场景:点击按钮,跳出一个对话框,对话框中显示两部分数据,来自两个不同的api接口,当这两部分数据都从接口获取到的时候,才让这个 数据加载中状态消失。让用户看到这两部分的数据。

合并请求结果并处理错误

单独处理一个请求的数据渲染和错误处理逻辑,有多个请求

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
function initLoad(){
// loading.show()
Promise.all([
getBannerList().catch(err=>err),
getStoreList().catch(err=>err),
getCategoryList().catch(err=>err)
]).then(res=>{
console.log(res) // ["获取轮播图数据失败啦", "店铺数据", "分类数据"]

if(res[0] == '轮播图数据'){
//渲染
}else{
//获取 轮播图数据 失败的逻辑
}
if(res[1] == '店铺数据'){
//渲染
}else{
//获取 店铺列表数据 失败的逻辑
}
if(res[2] == '分类数据'){
//渲染
}else{
//获取 分类列表数据 失败的逻辑
}

// loading.hide()
})
}

这里考虑使用Promise.allSettled也可以。

验证多个请求结果是否都满足条件

具体描述:表单的输入内容安全验证,多个字段调用的是同一个内容安全校验接口,只有所有都检验通过够才能够正常提交。

Promise.race

请求超时提示:点击按钮发请求,当后端的接口超过一定时间,假设超过三秒,没有返回结果,我们就提示用户请求超时,例如请求一张图片时超时提示

问题来源

px是一个相对单位,并不是说1px就一定等于1个物理像素,设备像素比不同,1px对应的物理像素数量不同。也就是说在不同设备、不同分辨率的情况下,1px所代表的物理像素数量不一样,因此1px在屏幕上显示的宽度也不一样。

设备像素、css像素、设备独立像素、设备像素比

设备像素:设备像素又称物理像素,是设备能够控制显示的最小物理单位

css像素:px是一个相对单位,相对的是设备像素(device pixel)一般情况,页面缩放比为1,1个CSS像素等于1个设备独立像素

设备独立像素:与设备无关的逻辑像素,代表可以通过程序控制使用的虚拟像素,是一个总体概念,包括了CSS像素,而至于1个虚拟像素究竟对应几个物理像素,就设计设备像素比(device pixel ratio)

设备像素比DPR = 设备像素/设备独立像素

当设备像素比为1:1时,使用1(1×1)个设备像素显示1个CSS像素

当设备像素比为2:1时,使用4(2×2)个设备像素显示1个CSS像素

当设备像素比为3:1时,使用9(3×3)个设备像素显示1个CSS像素

设备像素比可通过window.devicePixelRatio查询

实现一条1px的线

1
<div class="A"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  .A {
height: 1px;
width: 800px;
background-color: black;
}

@media screen and (-webkit-min-device-ratio: 2) {
.A {
transform: scaleY(0.5)
}
}
@media screen and (-webkit-min-device-ration: 3) {
.A {
transform: scaleY(0.333)
}
}

transform中的scale字段能够对元素进行缩放,当dpr=2时,(2 * 2)个物理像素对应1个设备独立像素,因此scale(0.5, 0.5)代表将1px缩放为原本的0.5倍,这样我们就得到了和dpr=1的设备上显示一样宽的1px

通过媒体查询针对不同dpr进行不同的缩放,保持各设备显示的一致性

实现一像素的边框

1
<div class="target">12345</div>
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
.target {
position: relative;
}
.target::after{
content: '';
width: 200%;
height: 200%;
position: absolute;
top: 0;
left: 0;
border: 1px solid #bfbfbf;
border-radius: 4px;
}
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.target::after {
-webkit-transform: scale(0.5,0.5);
transform: scale(0.5,0.5);
-webkit-transform-origin: top left;
transform-origin: left top;
}
}

@media screen and (-webkit-min-device-pixel-ratio: 2) {
.target::after {
-webkit-transform: scale(0.33,0.333);
transform: scale(0.333,0.333);
-webkit-transform-origin: top left;
transform-origin: left top;
}
}

transform-origin CSS属性用于更改一个元素变形的原点。默认原点是元素旋转中心。

官方描述

Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

从上面这段话中我们可以得知:

  • 几乎interface的所有特性都可以用type实现
  • interface可以添加新的属性,是可扩展的

区别一

针对第一点,参考官方对interfacetype的描述:

  • Interfaces are basically a way to describe data shapes, for example, an object.
  • Type is a definition of a type of data, for example, a union, primitive, intersection, tuple, or any other type.

interface用来描述数据的形状(data shapes)

至于什么是数据的形状呢? 例如二叉树中数据以分层的形式排布,每个元素最多由两个子元素;在链表中,数据以链式存储,顺序布局,这便是data shapes,结合数据本身,以及保留data shapes的相关操作(对于链表来说就是对链表节点的添加、删除等,不破坏原有结构),这三者就组成了数据结构。

type是数据类型的定义,如联合类型(A |B)基本类型交叉类型(A&B)、元组等,此外type 语句中还可以使用 typeof获取实例的类型进行赋值。

简而言之,**interface右边必须是 data shapes, 而type右边可以是任何类型。**

开头提到interface是可扩展的的,也是得益于声明合并,而type虽然通过extends可以达到类似的效果,但谈不上可扩展。官方描述中也提到:

the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

区别二

针对第二点,interface支持声明合并(declaration merging),type alias不支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person {
name: string;
}
interface Person {
age: number;
}
// 合并为:interface Person { name: string age: number}

type User = {
name: string;
};
type User = {
age: number;
};
// error: Duplicate identifier 'User'.

总结

主要有两点区别:

  1. interface右边只能是data shapes,而type右边涵盖的范围更大,还可以是联合类型(A |B)基本类型交叉类型(A&B)、元组等,也可以使用typeof
  2. interface支持声明合并,type不支持声明合并。

参考

TS Handbook

bottom type

首先, never是一个bottom type,这如何体现呢?

image.png
:✅表示 strictNullChecksfalse时的情况

neverunknown朝着两个相反的方向行进,所有的类型都可以赋值给unknown, never可以赋值给任何类型;unknown不能赋值给除any和自身之外的任何类型,除never本身外,任何类型都不能赋值给never

应用场景

  1. 用于从来不会返回值的函数

    这可能有两种情况,一是函数中可能死循环

    1
    2
    3
    4
    5
    function loop():never {
    while(true) {
    console.log('I always does something and never ends.')
    }
    }

    另外一种情况就是这个函数总是会抛出一个错误,因此也总是没有返回值

    1
    2
    3
    function loop():never {
    throw new Error('error!')
    }
  2. 穷尽检查(Exhaustiveness checking)

    对于一个联合类型,将其类型收窄为never

    1
    2
    3
    4
    5
    6
    7
    interface Foo {
    type: 'foo'
    }
    interface Bar {
    type: 'bar'
    }
    type All = Foo | Bar
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function handleValue(val: All) {
    switch (val.type) {
    case 'foo':
    // 这里 val 被收窄为 Foo
    break
    case 'bar':
    // val 在这里是 Bar
    break
    default:
    // val 在这里是 never
    const exhaustiveCheck: never = val
    break
    }
    }

    通过case 对可能的类型进行了相应处理,因此defaultval的类型是never,这也体现了never是一个底层类型:never只能赋值给never。如果之后联合类型All中添加了新的类型,但是在代码中忘记进行相应处理,那么就能提前暴露处错误,提醒开发者进行处理。

    never和void的区别

    1. 从赋值的角度来看,undefined可以赋值给void类型的变量,除了never本身,任何值都不能赋值给never类型,也就是说never意味着没有任何值。

      注:strictNullChecksfalse时,null类型也是可以赋值给void

    2. void 表示一个函数并不会返回任何值,当函数并没有任何返回值,或者返回不了明确的值的时候,就应该用这种类型。

      never表示一个函数从来不返回值,可能这个函数处于死循环,一直在运行,也可能这个函数运行过程中报错;never只能赋值给never,可以利用这个特性进行穷尽检查(Exhaustiveness checking)

注:

当基于上下文的推导,返回类型为void时,不会强制返回函数一定不能返回内容,也就是说当这样一个类型(type vf = () => void)被应用时,也是可以返回值的,只不过返回的值会被忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type voidFunc = () => void;

const f1: voidFunc = () => {
return true;
};

let a = f1() //let a: void

const f2: voidFunc = () => true;

let b = f2() //let b: void
const f3: voidFunc = function () {
return true;
};

let c = f3() //let c: void

可以看到a b c的类型都是void

但当一个函数字面量定义返回一个 void 类型,函数是一定不能返回任何东西的

1
2
3
4
5
6
7
function f2(): void {
return true; //Type 'true' is not assignable to type 'void'.
}

const f3 = function (): void {
return true; //Type 'true' is not assignable to type 'void'.
};

参考

ts handbook

strictNullChecks

共同点

首先是二者的相同点:unknownany都是顶层类型,也就是所有类型都可以赋值给unknownany

区别

不同点在于any相比,unknown是更符合类型安全原则的,使用any就意味着放弃了类型安全检查,此时你可以对一个any类型的的变量进行任何操作,但如果这个变量是unknown,你不能直接对它进行操作,因为unknown此刻类型是未知的,直接操作可能会出错,需要unknown进行类型收窄

传统功夫是讲究化劲儿的,any就是这样一股化劲儿,哪里不通怼哪里,化劲儿练到最后就可以将anyscript修炼到大成,也就是纯正的Javascript,即没有类型检查的阶段!

注意观察下面的例子:

使用 any 跳过了类型检查,不会报错;

1
2
3
function sayMyName(callback: any) {
callback()
}

同样是顶层类型,unknown 会有类型检查

1
2
3
function sayMyName(callback: unknown) {
callback()
} //(parameter) callback: unknown Object is of type 'unknown'.

虽然上述例子中,使用any时不会爆出类型错误,但是最终运行代码时还是可能会报错,比如运行 sayMyName(1); 但 使用unknown时,同样的代码,TS为我们指出了潜在的错误,这也是TypeScript的初衷,因此说:**any相比,unknown是更符合类型安全原则的**。

对使用unknown的情形进行类型收窄:

1
2
3
4
5
function sayMyName(callback: unknown) {
if(typeof callback === 'function') {
callback()
}
}

将unknown收窄到特定类型,就不会报错了。

也可以使用类型断言达到类似效果

1
2
3
4
5
6
let res: unknown = 123
let a: string = res as string //通过类型检查,但运行报错
const b: number = res as number

console.log(a.toLocaleLowerCase())
// [ERR]: a.toLocaleLowerCase is not a function

svg

可缩放矢量图形Scalable Vector Graphics,SVG),是一种用于描述二维的矢量图形,基于 XML 的标记语言, 这意味着可以使用任何文本编辑器(如记事本)创建和编辑SVG图像。

JPEGPNG这种传统的点阵图像模式不同,SVG格式提供的是矢量图,这意味着它的图像能够被无限放大而不失真或降低质量,并且可以方便地修改内容。

HTML <svg>元素是svg图形的容器。


svg demo

1
2
3
<svg id="svgelem" height="200">
<circle id="greencircle" cx="60" cy="60" r="50" fill="#1E81FF" />
</svg>

image.png

canvas

<canvas>本身只是相当于一块画布,不具有绘图能力,必须通过脚本(通常是JavaScript)动态地绘制图形,脚本充当画笔的角色。元素只是图形的容器, 必须使用脚本来实际绘制图形。Canvas有几种绘制路径、框、圆、文本和添加图像的方法


canvas demo

1
2
3
4
5
6
7
8
9
<canvas id="newCanvas" width="100" height="100" style="border:1px solid #000000;">
</canvas>

<script>
var c = document.getElementById('newCanvas');
var ctx = c.getContext('2d');
ctx.fillStyle = '#1E81FF';
ctx.fillRect(0, 0, 100, 100);
</script>

image.png

二者的区别

SVG是一种基于XML中的2D图形的语言。

Canvas通过脚本动态绘制2D图形。

SVG是基于XML的,这意味着每个元素都在SVG DOM中可用, 可以为元素附加JavaScript事件处理程序。在SVG中,将每个绘制的形状记住为对象。如果更改了SVG对象的属性,则浏览器可以自动重新呈现形状。

Canvas由像素呈现,一旦图形在画布中绘制完成,浏览器撒手不管了。如果需要更改其位置,则需要重新绘制整个场景,其中许多对象会被频繁重绘。

详细对比如下:

SVG Canvas
不依赖分辨率(矢量图) 依赖分辨率(位图)
每一个图形都是一个 DOM元素 单个HTML元素,相当于<img>
支持事件处理器 不支持事件处理器
适合大型渲染区域的应用程序(谷歌地图) 文本渲染能力差
可以通过脚本和CSS进行修改 只能通过脚本修改
对象数量较小 (<10k)、图面更大时性能更佳 图面较小,对象数量较大(>10k)时性能最佳
不适合游戏应用 适合图像密集型的游戏应用

兼容性

Can I use Svg ?

image.png

Can I use Canvas ?

image.png

参考

Can I use

W3cSchool

MDN