0%

TypeScript分享

在分享之前我先回答一下为什么要使用 TypeScript(以下简称:TS)?前端的 TS 是什么?我们如何在项目中更好的使用 TS?


概念

TS 是什么?

首先 TS 是 JavaScript(以下简称:JS)的一个超集。我这里先回顾一下高中的超集概念:

如果一个集合 S2 中的每一个元素都在集合 S1 中,且集合 S1 中可能包含 S2 中没有的元素,则集合 S1 就是 S2 的一个超集,反过来,S2 是 S1 的子集。 S1 是 S2 的超集,若 S1 中一定有 S2 中没有的元素,则 S1 是 S2 的真超集,反过来 S2 是 S1 的真子集。

这里的意思就是 TS 包含了所有的 JS 特性,并且拥有一些 JS 里面没有的特性,能够编译成 JavaScript 代码。其中最大的特性就是在JS变量是没有类型的,只有值是有类型的TS变量也具有类型,其核心能力是在代码编写过程中提供了类型支持,以及在编译过程中进行类型校验。

前端的 TS 是什么?

在前端业务中使用 TS 更多的在于对类型的规定,某些高级的 TS 特性(比如抽象类、命名空间)使用场景较少。即使是泛型这块使用的最多场景也就在前后端接口请求这块。

为什么要使用 TS?

可能有些人觉得写 TS 要写很多类型上面的代码,加大了前端的开发量,其实不然,在我看来动态语言的最大的弊端就是调试困难,因为动态语言的变量是无类型的,变量指向的内容就不确定,变量的引用只有在运行时才知道它具体指向哪里,才知道这个变量包含哪些内容。如果变量有类型,不用等到运行期间,在编码的时候就知道某个对象是不是我们期待的那个对象;其次,在后期迭代的时候回顾先前的代码也比较方便,当项目有新人到来的时候也更方便新人了解项目。前期的小投入可以换来后期大回报。最后 TS 的是 JS 的一个超集,对于前端工程师来说学习成本很低。

我们如何在项目中更好的使用 TS?

怎么叫用的好?其实很简单,当我们拿到一个变量时,我可以很清楚的知道这个变量是什么,它指向哪,如果他是高级类型,我们在`vs code或者其他IDE的时候能智能提示他有什么属性或者方法。


在编译 TS 时,即使报错,默认也不会中断编译

基本类型

基本类型和 JS 类似,多了几个特有的enum,unknown,void,never,tuple,any

  • boolean

  • number

  • string

  • object

  • number[] / Array<number> 数组

    • tuple 元组:已知元素数量类型的数组,
  • enum

    • 枚举,可以通过键名找到对应键值,也可以通过键值对到相应的键名。

    • 枚举的值只能是 number 或者 string 类型

    • ```typescript // ts enum Color { // 默认起始值为0 // Red = 0, Red, // 也可以显示的指定对应的值, Green = 100, // 后面的值为前面的值+1 101 Yellow, // 当指定为非number类型时,此时不能再通过值找到对应的键 White = 'White', // 且后面的属性的值不会依次增加,如果不显示的指定值,编译会报错 Black, }

      // Color.Red === 0 // Color.Green === 100 // Color.Yellow === 101

      // 编译后的结果 var Color ;(function (Color) { // 默认起始值为0 // Red = 0, Color[(Color['Red'] = 0)] = 'Red' // 也可以显示的指定对应的值, Color[(Color['Green'] = 100)] = 'Green' // 后面的值为前面的值+1 101 Color[(Color['Yellow'] = 101)] = 'Yellow' // 当指定为非number类型时,此时不能再通过值找到对应的键 // 且后面的属性的值不会依次增加 Color['White'] = 'White' Color[(Color['Black'] = void 0)] = 'Black' })(Color || (Color = {}))

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      - void

      - undefined

      - null

      - never

      - 永远不会返回。很少用到

      - ```typescript
      function func(): never {
      while (true) {}
      }

  • unknown

  • any

对于anyunknown,两者最大的区别是,unknown为任何类型的父类型,any既是任何类型的父类型,也是任何类型的子类型

即任意类型可以转换为 unknown


鸭子类型

在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

TypeScript 采用了所谓的 “鸭子类型”策略。当两个类型具有相同的属性以及方法时,它们就可以看作是同一类型。

如何定义一个变量

1
2
3
4
5
6
7
8
9
10
11
// c 语言
typedef struct _Point {
int x;
int y;
} Point;

int main() {
Point p;
p.x = 1;
p.y = 2;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// c++
struct Point {
int x;
int y;
};

struct Person {
int age;
};

struct Student : public Person {
Student() {
this->age = 18;
}
char name[20];
};

int main() {
Point p;
p.x = 1;
p.y = 2;
// 多态
Person *person = new Student();
}
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
// java
class Point {
int x;
int y;
}

interface Person {
String: name;
}

class Student implements Person {
private String name;

Student() {
this.name = ""
}
}

public class Main {
public static void main(String[] args) {
// 多态
Point p = new Point();
p.x = 1;
p.y = 2;
Person person = new Student();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// typescript
interface Point {
x: number
y: number
}

interface XPoint {
x: number
y: number
}

function fn(p: Point) {
console.log(p.x, p.y)
}
const p: XPoint = {
x: 100,
y: 100,
}
// p是XPoint 类型,但fn接收 Point 类型,由于TS的鸭子类型策略,所以会当他们为一种类型
fn(p)

接口 interface

用来声明类型和接口的

TS 在前端的接口可能又拥有另外一重意思,在其他静态类型语言里面,接口是一种抽象类型,它用来制定一些规范,你要实现我的接口,就必须遵循我的规定。

在其它静态语言中(比如 Java,C#)的接口的特性,TS 里面也有,但在前端(浏览器层)的 TS 里面,接口interface 更多的是用来表示某一个变量它具体有哪些内容。多态的这一特性很少有使用到。

typeinterface

  • type 类型别名
    • 可以用来给已有的类型取一个别名
    • 只能声明一次,多次声明同一种类型报错
  • interface 接口/类型定义
    • 多次声明同一个接口会进行接口合并

typeinterface 的区别在于,能用interface表示的地方,一定能用type表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义类型
interface BaseUser {
// 一个只读的id属性,为number类型
readonly id: number
}
// 现次定义 会进行声明合并,此时的 BaseUser 包含两个属性,id 和name
interface BaseUser {
// 一个只读的id属性,为number类型
name: string
}

/* 可以理解为给
{
readonly id: number
}
这种类型,取个别名,叫BaseUser
*/

type BaseUser = {
// 一个只读的id属性,为number类型
readonly id: number
}

属性修饰

  • readonly 只读
  • ? 可为空
1
2
3
4
5
6
7
8
9
10
interface Point {
x: number
y?: number
// y: undefined | number
}
// y?: number;
// y: undefined | number
// 这两种语义不一样
// 第一个表示,y属性可以有,可以没有
// 第二个表示y一定有,他的值为 undefined 或者 number

类型断言/强制类型转换

类型断言,这里我习惯称之为强制类型转换。

  • ```typescript interface Point { x: number y: number } // 报错,因为ts认为{}是一个对象,他没有x,y这两属性,就不能赋值给p const p: Point = {} // 即使{}没x,y 这里通过断言的形式告诉编译器{}它的值就是一个Point类型的值 const p1: Point = {} as Point const p2: Point ={}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    **可以索引类型**

    ```typescript
    // 索引类型
    // 类型限定
    interface Person {
    // 属性扩充,表示Person类型的属性只能是number 或者 string类型的,并且可以添加未在内部声明的字段
    [propName: string]: number | string
    age: number
    name: string
    address: string
    }
    const p: Person = {
    age: 18,
    name: 'Pic',
    address: '',
    // 原定义中没有,但[propName: string]: number | string 表示可以进行属性动态添加,只要满足属性为number或者string
    zipCode: '111',
    }
    p.gender = 0

函数类型

1
2
3
4
5
6
7
8
// 函数类型, 函数代有特定属性(cancel)
interface ScrollDebounceFunction {
(event: Event): void
cancel: () => void
}

// 函数类型, 函数接受一个Event,无返回值
type ScrollDebounceFunction = (event: Event) => void

继承

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
// 定义类型
interface BaseUser {
// 一个只读的id属性,为number类型
readonly id: number
}

// 继承自BaseUser,会相应的继承对应的属性
interface User extends BaseUser {
// 这里存在一个继承自BaseUser的id属性并且是可读的
// readonly id: number

// 可以取消父属性的只读限制
// id: number

// 不能修改同名的父属性的类型
// id: string // 报错

// 不能修改同名的父属性必选限制, 但可以将父属性的可选改成必选
// id?: number // 报错

// username 为string类型或者undefined
username: string | undefined
// 表示address是可选的,它要么值为string类型的,要么不存在
address?: string
}

// 多继承
interface Base1 {
// id: number;
age: number
}
interface Base2 {
id: string
address: string
}

interface Person extends Base1, Base2 {
name: string
}

在多继承时,会继承所有父级的属性,当多个父级有相同的属性,但属性的只读、可选限定不同时,会继承失败,报错。当父子都有相同的属性时,子属性与父属性的类型不同时,类型相同时,以子属性的只读限定为准。但当属性为必选限制,子属性就不能改成可选限定。


泛型

我理解的泛型是为了增强类型的可复用性,在定义某一类型时,某个属性的具体类型不确定,但这个类型的基本结构可预估。

内置基础泛型

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
47
48
49
50
interface Admin {
name: string
age: number
address: string
role: string
}
/**
* 让该类型的所有属性为可选的
*/
type Partial<T> = {
[P in keyof T]?: T[P]
}
type A = Partial<Admin>
// 等同于
// interface A {
// name?: string
// age?: number
// address?: string
// role?: string
// }
/**
* 让该类型的所有属性为必选的
*/
type Required<T> = {
[P in keyof T]-?: T[P]
}
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never
type ConstructorParameters<T extends new (...args: any) => any> =
T extends new (...args: infer P) => any ? P : never
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any
type InstanceType<T extends new (...args: any) => any> = T extends new (
...args: any
) => infer R
? R
: any
interface ThisType<T> {}

几个解释

上面的T 称之为 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

extendsextends在形参列表里面时,表示的对传入的类型 T 的泛型约束。T extends (...args: any) => any 这里对 T 的限制就是T只能是函数类型,

infer 代表推理。并且只能的 extends 一起用

这里详细说明一下 infer 的作用

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 这里可以先对这个表达式进行拆分
// 我们先定义一个函数类型, 用来比较两个的数的大小,如果第一个参数大于第二个参数就返回true
type AMoreThanB = (a: number, b: number) => boolean
// 我们再定义一个比较简单的ReturnType
type ReturnType<T extends AMoreThanB> = T extends AMoreThanB ? boolean : any
// T extends AMoreThanB ? boolean : any;
// 这个表达式肯定会返回一个类型,
// 这里的? : 有点类似三元运算符,
// 不防这样理解,当满足 T extends AMoreThanB时返回boolean,否则返回any

// 再使用这个ReturnType, 因为对T的限定只能是AMoreThanB,我们就传入一个AMoreThanB,然后就得到了Rt
type Rt = ReturnType<AMoreThanB>
// 这里肯定满足 T extends AMoreThanB,因为我们传入的T就是AMoreThanB,所以得到的Rt一定就是boolean,

// 如果我们传入一个其他的类型
type Rt2 = ReturnType<boolean>
// 这个时候就报错了,因为我们对T的限制他只能是AMoreThanB类型,或者这么说限制传入的这个函数必须要两个number类型的参数,并且返回一个boolean类型值
// 现在我们放宽点
type ReturnType<T extends (...arg: any) => any> = T extends AMoreThanB
? boolean
: any
// 现在T可能是任何一个函数

// 我们再定义一个新的函数类型
type TimeoutFuc = (callback: AMoreThanB, delay: number) => number

// 这个时候我们再次使用这个ReturnType
type Rt3 = ReturnType<TimeoutFuc>
// 由于TimeoutFuc 不满足 T extends AMoreThanB ,所有返回any,这里的Rt3就是any;

// 还没讲到infer,不着急,
// 我们再把ReturnType改进一下
type ReturnType<T extends (...arg: any) => any> = T extends (...arg: any) => any
? boolean
: any
// 这个时候不管传入什么样的函数返回都是boolean, 因为T extends (...arg: any) => any也一定满足。

// 我们想返回一些动态的类型怎么办,或者我们想返回一些和T相关的类型
// 这个时候 infer就出来了
type ReturnType<T extends (...arg: any) => any> = T extends (
...arg: any
) => infer R
? R
: any
// 这里我们将any 换成了infer R, 并当满足这个条件时把R返回

type Rt4 = ReturnType<TimeoutFuc>
// 当我们再次使用时,这里的TimeoutFuc,满足 T extends (...arg: any) => infer R 。
// 并且我们还拿到了TimeoutFuc这个函数的返回类型并赋给了R,然后返回R,所以,这里的Rt4就是TimeoutFuc这个函数的返回类型:num ber

type ReturnType<T extends (...arg: any) => any> = T extends (
arg1: infer T,
...arg: any
) => infer R
? T | R
: any

// 我们加大难度,这个时候的ReturnType返回的是传入函数类型的第一个参数类型和函数的返回类型的 并
// 这里就等同于
type Rt5 = ReturnType<TimeoutFuc>
// type Rt5 = number | AMoreThanB;

// infer 的作用就是推断相应的类型.

继承,接口,多态就不讲了。

讲一下类的装饰器,装饰器可以对原有的类,或者属性,方法,在编译的时候进行加强。

执行顺序 属性装饰器、参数装饰器、方法装饰器、类装饰器

类装饰器是作用于当前这个类,属性装饰器、方法装饰器、参数装饰器

作用于这个类的 prototype

执行时机 编译时

属性装饰器

1
2
3
function fieldDescriptor(target: any, fieldName: string): void {
console.log('fieldDescriptor', typeof prototype, prototype)
}

参数装饰器

1
2
3
4
5
6
7
function paramsDescriptor(
target: any,
propertyKey: string,
parameterIndex: number
): any {
console.log('paramsDescriptor', propertyKey, parameterIndex)
}

方法装饰器

1
2
3
function methodDescriptor(prototype: Demo, methodName: string): void {
console.log('methodDescriptor', typeof prototype, prototype)
}

类装饰器

1
2
3
function classDescriptor(constructor: any): void {
console.log('classDescriptor', typeof constructor, constructor)
}
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 fieldDescriptor(target: any, fieldName: string): void {
console.log('fieldDescriptor', typeof target, target)
}
function paramsDescriptor(
target: any,
propertyKey: string,
parameterIndex: number
): any {
console.log('paramsDescriptor', propertyKey, parameterIndex)
}

function methodDescriptor(prototype: Demo, methodName: string): void {
console.log('methodDescriptor', typeof prototype, prototype)
}

function classDescriptor(constructor: any): void {
console.log('classDescriptor', typeof constructor, constructor)
}

@classDescriptor
class Person {
@fieldDescriptor
name: string

@methodDescriptor
log(@paramsDescriptor p: string) {
console.log(this.name)
}
}

编译后

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
47
48
49
50
51
52
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r =
c < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc,
d
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc)
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i]))
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
return c > 3 && r && Object.defineProperty(target, key, r), r
}
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex)
}
}
function fieldDescriptor(target, fieldName) {
console.log('fieldDescriptor', typeof target, target)
}
function paramsDescriptor(target, propertyKey, parameterIndex) {
console.log('paramsDescriptor', propertyKey, parameterIndex)
}
function methodDescriptor(prototype, methodName) {
console.log('methodDescriptor', typeof prototype, prototype)
}
function classDescriptor(constructor) {
console.log('classDescriptor', typeof constructor, constructor)
}
var Person = /** @class */ (function () {
function Person() {}
Person.prototype.log = function (p) {
console.log(this.name)
}
__decorate([fieldDescriptor], Person.prototype, 'name')
__decorate(
[methodDescriptor, __param(0, paramsDescriptor)],
Person.prototype,
'log'
)
Person = __decorate([classDescriptor], Person)
return Person
})()