TypeScript笔记

1,222 阅读16分钟

基本数据类型

TS 中继承了所有 JS 中的基本数据类型,并做了一些拓展。

// es 中的基本数据类型
boolean
number
string
array
Function
object
symbol
undefined
null

// TS 扩展的类型
void
any
never
元组
枚举
高级类型

基本类型

const b: boolean = true;
const num: number = 2

数组类型

const arr1: number[] = [1, 2];
const arr2: Array<number | string> = [1, 'ts'];

元组

元组只允许按照定义时的类型和长度添加元素

const tuple: [number, string] = [1, 'ts'];

// 元组的越界问题
tumple.push(3);  // success
console.log(tumple[2]);  //error

元组可是使用原生的方法增加元素产生越界。但是无法访问越界元素。

函数类型

在ts中函数不仅可以约定参数类型还可以定义返回值类型。

const add = (x: number, y: number) => x + y;

以上是一个常见函数。而且此时函数会默认知道自己返回的是 number 类型。这叫类型推断

此时如此赋值就会报错

const text: string = add(1, 3); // error

对象类型

let obj: object = {x: 1, y: 2};
obj.x = 3 // error ????
let obj2: {x: number, y number} = {x: 1, y: 2};
obj.x = 3 // success

undefined & null

let un: undefined = undefined;
let nu: null = null;
un = 1 // error

let num: number = 1;
num = undefined; // success

以上代码可以看出 undefined 和 null 是所有类型的子类型。

如果出现其他类型复制 undefined 失败的情况。可以将 tsconfig 中的 strictNullChecks 设置为 false。

void

void 表示:一个表达式的返回值是 undefined

let fun(x: number) => void

此时 testFun 的返回值为 undefined。

fun = (x) => x * 3 // error

any

任意类型

never

never 代表永远不会有返回值的类型。

注意和 void 的区分。

  • void:有返回值,返回 undefined
  • never:没有返回值
const fun = () => {
    let x = 1;
    while(true) {
        x += 1;
    }
}

枚举

在 ts 中枚举类型细分下来有三种

  1. 数字枚举
  2. 字符串枚举
  3. 异构枚举

数字枚举

一下代码声明了一个数字类型的枚举。

enum Zonglian {
    cup,
    pen,
    computer
}

Zonglian[0]  // cup
Zonglian.pen // 1

// 如果你对索引进行一些干预则会有不同的效果
enum ZonglianAgain {
    cup = 1,
    pen,
    computer = 5,
    banana,
}

ZonglianAgain.pen   // 2

ZonglianAgain[0]    // ?
ZonglianAgain.banana // ?

ts 是如何实现数字枚举的?

var Zonglian;
(function (Zonglian) {
    Zonglian[Zonglian["cup"] = 0] = "cup";
    Zonglian[Zonglian["pen"] = 1] = "pen";
    Zonglian[Zonglian["computer"] = 2] = "computer";
})(Zonglian || (Zonglian = {}));

这种方法学名叫做反向映射

字符串枚举

enum Message {
    success = '成功',
    fail = '失败'
}

Message.success // 成功
Message['成功'] // undefined

以上代码可以看出,字符串枚举类型是不支持反向映射的。

异构枚举

异构枚举:数字枚举和字符串枚举的混合装。

enum Answer {
    N,
    Y = 'Yes'
}

枚举的其他特性

enum TestEnum {
    // 常量型枚举:在编译时就已经得出结果
    a,
    b = 1 + 2,
    // 计算型枚举:在运行时才有结果。定义在此后的枚举值一定要有初始值
    d = 'abc'.length(),
    e, // error
}

常量型枚举在编译时候会被移除。

但我们不需要对象。仅仅需要对象值的时候就可以使用常量枚举以减少代码编译后的体积。


接口

接口分为以下类型

  1. 对象类型接口
  2. 函数类型接口

对象类型接口和鸭式辨形法

来看一个最基本的接口用法。

interface List = {
    id: number;
    name: string;
}

function handleList(res: List[]) {
    res.forEach(listItem => {
        console.log(listItem.name)l
    })
}

以上代码中 res 可能是后端返回的一段数据,也可能是你自己写的一段 JSON。

const res = [
    {
        id: 1,
        name: 'res1'
    },
    {
        id: 2,
        name: 'res2',
        age: 26
    }
]

handleList(res); // success

以上代码中,数据多了 age 属性。但是 ts 依然没有报错。这个就叫做鸭式辨形法。只要 res 的元素中包含了 List 的必要属性,那么 ts 就认为他是一段正确的数据。

如果变成这样那么就会有问题了。

handleList(
    [
        {
            id: 3,
            age: 26
        }
    ]
)
// error

因为以上代码中,没有 name 这个必要的属性所以报错了。

函数类型接口

声明函数接口的方式

变量法:

let add: (x: number, y: number) => number;

接口法:

interface Add {
    (x: number, y: number): number
}

类型别名:

type Add = (x: number, y: number) => number;

注意:以上三种方式只是对函数进行了定义,而没有实现。

实现函数

let add: Add = (a, b) => a + b;

混合类型接口

混合类型接口意思是:一个接口既可以定义一个函数,也可以像对象一样拥有属性和方法。

interface MixinType {
    (): void;
    type: string;
    success(): void;
}

// 实现接口
let mixinType: MixinType = () => {};
mixinType.type = '1';
mixinType.success = () => {};

其实以上代码定义完成后,编译器依然会报错。所以你可能需要一个类型断言。

let mixinType: MixinType = (() => {}) as MixinType;

接口的继承

类的继承,可以抽离公共的方法也可以将多个接口合并成一个方法。

interface Human {
    name: string;
    eat(): void;
}

interface Man EXTENDS Human {
    beard: string
}

interface Tom extedns Man {}

let tom: Tom {
    name: '',
    beard: ''
}

泛型

函数泛型

我们可以利用函数重载来实现一个类型灵活的函数。

function log(value: string): string {
    console.log(value);
    return value;
}

function log(value: number[]): number {
    console.log(value);
    return value;
}

function log(value: any): any {
    console.log(value);
    return value;
}

或者也可使用联合类型。

function log(value: string | number[]): string | number {
    console.log(value);
    return value;
}

但是以上的方法依然不够灵活。现在我们可以使用泛型。

泛型允许你在调用的时候才传入真正的类型,从而实现最大的灵活性。

function log<T>(value: T): T {
    console.log(value);
    return value;
}

log<number[]>([0, 1]);

泛型同时也支持多个参数

function log<T, U>(one: T, Two: U) {
    console.log(one, two);
}

泛型也可以用来约束接口成员

interface Log<T> {
    (value: T): T
}
// 注意 一旦使用了泛型接口,类型推断就不再有作用,必须传入具体的类型。
let myLog: Log<number> = (value) => value

泛型类

先来看一个最简单的泛型类的实现。

class Log<T> {
    run(value: T) {
        console.log(value);
        return value;
    }
}

let log1 = new Log<number>();
log1.run(2);
let log2 = new Log();
log2.run('ttt');
log2.run({ test: 'test' })

注意:泛型类不能用于静态成员,以下代码是会报错的。

class Log<T> {
    static run(value: T) {
        XXXX
    }
}

泛型约束

先来看一段代码

function log<T>(value: T): T {
    // 编译器会报错:T 上不存在 length 属性
    console.log(value, value.length);
    return value;
}

此时我们就要用到类型约束了。

interface Length {
    length: number;
}

function log<T ectends Length>(value: T): T {
    console.log(value, value.length);
    return value;
}

此时编译器就不会报错了。但是同时 T 也不可以再传任意类型的参数进来。参数必须是有 length 属性的参数。此时我们就说 T 受到了类型约束。


高级类型

高阶类型大致可以分为以下几种

  1. 交叉类型
  2. 联合类型
  3. 索引类型
  4. 映射类型
  5. 条件类型

交叉类型

将多个类型合并为一个类型。新的类型将具有所有类型的特性。

所以交叉类型特别适合对象混入的场景。

interface DogType {
    run(): void
}

interface CatType {
    jump(): void
}

let pet: DogType & CatType = {
    run() {},
    jump() {}
}

可以看到上面的 pet宠物 就是 狗 和 猫的交叉类型。

注意:交叉类型并不是取所有类型的交集,而是取所有类型的并集。

如果我们在交叉类型中定义了同样的方法会发生什么呢?看看以下代码。

interface DogType {
    run(): void
    eat(x: number): number
}

interface CatType {
    jump(): void
    eat(y: string): string;
}

let pet: DogType & CatType = {
    run() {},
    jump() {},
    // 此时的 eat 应该怎么实现?
}

接口属性分为两种,如果是基本类型比如:

interface A {
    a: number
}

interface B {
    a: string
}

let foo: A & B = {
    a:
}

此时 foo 的属性 a 应该是 never 即 number & string 的类型。但是 ts 没有任何值可以赋值给 never 类型,所以无论怎么赋值都会失败。

如果属性是函数那么情况就有所不同。

interface A {
    f(a: number): number
}

interface B {
    f(a: string, b: string): string
}

let foo: A & B = {
    f(a: never) {
        return a;
    }
}

// 或者
let foo: A & B = {
    f(a: any) {
        return a;
    }
}

我们看到在代码中没有报错。

在交叉类型中,函数 f 实际发生了函数重载。为了同时兼容 A 和 B 中的定义,参数要选择最少的。参数的返回值要取 number & string 即 never 或者 any。

但是实际上我们应该避免以上这种写法。

联合类型

当声明的类型并不确定,可能是多个类型中的中的一个,叫做联合类型。联合类型也可以分为不同的种类。

基本类型的联合类型

let a: number | string = 1;

// 有时候我们不仅需要规定类型。还要求将取值确定在一定范围内
let b: 'a' | 'b' | 'c'
let c: 1 | 2 | 3

对象的联合类型

此处我们要用到交叉类型中的两个接口

class Dog implements DogType {
    run() {}
    eat() {}
}

class Cat implements CatType {
    jump() {}
    eat() {}
}

emun Master { Boy, Girl }

function getPet(master: Master) {
    let pet = master === Master.Boy ? new Dog() : new Cat();
    // 此时 pet 就是一个联合类型
    return pet;
}

我们来看一下在代码中的实际表现

可以看到当我们输入 pet. 的时候编译器只显示了 eat,而如果我们访问其他的属性则会报错。

所以我们可以得知。联合类型听起来是 Dog 和 Cat 的并集,实际上是其交集。正常情况下只能访问到两个类型的共有属性。

可区分的联合类型

利用联合类型的共有属性。我们就可以建立一系列的分支区块。

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.width * s.height;
    }
}

以上代码中 area 是一个计算形状面积的函数。表面上看起来没有什么问题,但是如果我们此时扩展一个类型就会发现有点问题。

我们增加了一个圆形。调用了 area 函数进行计算并打印结果。此时打印出了 undefined。很明显此时的 switch 语句没有覆盖到所有的情况,但是是我们的 TS 并没有报错。怎么办?

  1. 我们可以强制规定一个类型返回

此时 TS 就会检查 switch 是否覆盖到了所有的情况。

  1. 利用 never 类型

default 函数的作用就是检查 s 是不是 never 类型。如果 s 是 never 类型说明 case 分支已经覆盖了所有的情况 default 函数永远都不会被执行。如果 s 不是 never 类型就说明 case 分支有遗漏就会报错。

索引类型

先来看一个场景:我们需要在一个对象中抽取一些属性值生成一个数组,以下是代码。

let myObj = {
    a: 1,
    b: 2,
    c: 3
}
function getValues(obj: any, keys: string[]) {
    return keys.map(key => obj[key]);
}

执行代码会看到以下的结果。

我们访问了两个 myObj 中不存在的属性,但是 TS 没有报错。如果才能让 TS 帮助我们对类型进行一些检查呢?此时就需要用到索引类型。

请先了解如下概念

  1. 索引类型的查询操作符
// keyof T:表示类型 T 的所有公共属性的字面量的联合类型
interface Obj {
    a: number;
    b: string;
}
let key: keyof Obj

  1. 索引访问操作符
interface Obj {
    a: number;
    b: string;
}

// T[k] 表示 T 的 属性 K 所代表的的类型
let value: Obj['a']  // 此时 value 的类型就是 number

  1. 泛型约束
// T extends U 表示泛型变量可以继承某个类型,从而得到一些属性

有了以上三个概念我们就可以改造 getValues 函数了。

let myObj = {
    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]);
}

我们看看效果

由此可以看到,索引类型可以实现对【对象属性】的查询和访问。然后配合泛型约束我们就可以建立【对象】【对象属性】【属性值】之间的约束关系。

映射类型

通过映射类型,我们可以通过一些旧的类型生成新的类型。来看看代码

interface Obj {
    a: string;
    b: number;
    c: boolean;
}
type ReadOnly<T> = {
    readonly [P in keyof T]: T[P]
}
type ReadOnlyObj = ReadOnly<Ojb>

我们来看看有什么效果

此时 ReadOnlyObj 和 Obj 中的属性相同但是全都变成了只读属性。我们来看看 type ReadOnly 做了什么。

// readonly 是一个可索引的泛型接口
type ReadOnly<T> = {
    // keyof T: T 中所有属性字面量的联合类型
    // P in:相当于执行了一次 for in 操作
    // T[P]:表示 T 中的属性 P 所指的类型
    readonly [P in keyof T]: T[P]
}

其实 TS 已经内置了很多的映射类型,包括以上的 ReadOnly。可以查看 node_modules/typescript/lib/lib.es5.d.ts 中的内容获得更多的信息。以下列举了几个可能常用的映射类型。

// 只读:将 Obj 的所有属性变为只读
type ReadOnlyObj = Readonly<Obj>
// 可选:将 Obj 的所有属性变为可选属性
type PartiaObj = Partial<Obj>
// 抽取:抽取了 Obj 中的 a b 属性
type PickObj = Pick<Obj, 'a' | 'b'>

以上三种类型官方统一称为同态也就是不会引入新的属性

type RecordObj = Record<'x' | 'y', Obj>

Record 就是一个非同态类型

ts 的其他特性

ts的命名空间

在 js 中命名空间可以避免全局污染,但是在 es6 引入模块化概念后命名空间就比较少被用到了。在 ts 中依然实现了这个特性,虽然在模块系统中我们不用考虑全局污染的问题,但是如果使用了全局类库,命名空间依然是比较好的解决方案。

命名空间的基本使用

命名空间使用 namespace 关键字。

/*
 Shape 命名空间内可以定义任意多的变量,但是只能在 Shape 内可见,如果想让外部使用就需要 export 导出。
*/
namespace Shape {
    const mp = Math.PI;
    export function circle(r: number) {
        return pi * r ** 2
    }
}
Shape.circle(2);

命名空间的拆分

命名空间的拆分是可以跨文件的。如果上一段代码是 circle.ts 那么我们在另一个文件 square.ts 中声明一个同名的命名空间,只需要用三斜线指令指定引用就可以正常使用 circle.ts 中的方法了。

/// <reference path="circle.ts" />
namespace Shape {
    export function square(x: number) {
        return x ** 2;
    }
}
Shape.circle(3);
Shape.square(5);

命名空间是如何实现的

我们直接来看看代码的编译结果就可以了

var Shape;
(function (Shape) {
    var pi = Math.PI;
    function circle(r) {
        return pi * Math.pow(r, 2);
    }
    Shape.circle = circle
})(Shape || (Shape = {}))

Shape 被编译成了一个立即执行的函数,函数形成了一个闭包,pi 是一个私有成员,而 export 出的 circle 则被挂载到了一个全局变量上。

命名空间的别名

我们可以给命名空间中的方法起一个别名方便我们引用。

import circle = Shape.circle;
circle(2);

注意此处的 import 和模块化引用没有任何关系。

ts的声明合并

声明合并是 ts 中的独特概念。概念是:编译器会把程序多个地方具有相同命名的声明合并为一个声明。好处就是当你声明了多个同名接口,在实现的时候你将会同时对多个接口有感知能力,可以避免对接口成员的遗漏

接口的声明合并

先来看以下代码

interface A {
    x: numnber;
}
interface A {
    y: number;
}

let a: A {
    x: 1,
    y: 2
}

以上代码中,接口 A 并没有出现覆盖的情况而是被合并到了一起。如果该代码是一个全局性的文件,两个 interface A 甚至可以不在同一个文件中也可以实现声明合并。

注意,接口的声明合并中如果重复声明非函数的方法,要求类型相同

interface A {
    x: number;
    y: string;
}
interface A {
    y: number; //error 程序报错
}

对于重复声明的函数方法,每个方法都会变成函数重载

interface A {
    x: number;
    fun: (arg: number): number;
}

interface A {
    y: number;
    fun (arg: string): string;
    fun (arg: number[]): numnber[];
}

let a: A {
    x: 1,
    y: 2,
    fun(arg: any) {
        return arg
    }
}

以上代码中对于 fun 就是实现了函数的重载。在 ts 中函数的重载是由解析顺序的。在正常情况下会按照从最早的声明开始寻找。但是在接口合并中规则稍有变化。同一接口中从早声明到晚声明,不同接口中从晚声明到早声明,如果函数的参数是一个字符串字面量的话该声明会被提升到最顶端。按照以上的规则在刚在的示例代码中,函数重载的查找规则是:

interface A {
    x: number;
    fun (arg: number): number;  // 5
    fun (arg: 'text1'): number; // 2
}

interface A {
    y: number;
    fun: (arg: string): string;  // 3
    fun: (arg: number[]): numnber[];  // 4
    fun (arg: 'text2'): number; // 1
}

工程篇

tsconfig 配置

本节列举了一些ts常见的配置

文件选项

{
    // 继承其他的 config 文件
    "extends": "./tsconfigBase",
    // 编译器需要编译的单个文件的列表
    "files": [
        "src/test.ts"
    ],
    // 编译器需要编译的文件或者目录
    // include 和 files 是会合并的
    "include": [
        "src", // src 下的所有文件type RecordObj = Record<'x' | 'y', Obj>
        "src/*/*" // src 二级目录下的文件
    ],
    // 编译器需要排除的文件或者目录,默认排除 node_modules 和 所有声明文件
    "exclude": [
        "src/lib"
    ],
    // 不太好用啊!保存文件的时候让编译器自动编译 vs code 目前不支持
    "compileOnSave": true
}

编译相关

编译相关的选项有100多个。一下列举常用的选项。

{
    // 增量编译
    "incremental": true,
    // 增量编译文件的储存位置
    "tsBuildInfoFile": "./buildFile",
    // 打印诊断信息
    "diagnostics": true,
    
    // 目标语言版本
    "target": "es5",
    // 生成代码的模块标准
    "module": "commonjs",
    // 将多个相互依赖的文件生成一个文件,通常用于生成 AMD 模块
    "outFile": "./app.js",
    
    // TS 要引用的库的声明文件
    // 如果不声明 lib ts 也会默认导入一些 lib 声明,例如 target 为 es5 的时候默认导入以下声明
    // lib: ['dom', 'es5', 'scripthost']
    // 如果我们想使用更新的方法例如 [1, 2, 3, [4, [5]]].flat() 则要引入新的类库
    // lib: ['dom', 'es5', 'scripthost', 'es2019.Array']
    "lib": [],
    
    // 允许编译 JS JSX 文件
    // 注意该属性通常要和 exclude 同时使用,否则可能会编译很多无用的文件
    "allowJs": true,
    // 允许在 JS 文件中报错
    "checkJs": true,
    // 指定输出目录
    "outDir": "./",
    // 指定输入文件目录
    "rootDif": "./",
    
    // 自动为编译的文件生成声明文件
    "declaration": true,
    // 声明文件的路径
    "declarationDir": "./d",
    // 只生成声明文件
    "emitDeclarationOnly": true,
    // 生成目标文件的 sourceMap
    "sourceMap": true,
    // 生成 inline 的 sourceMap
    "inlineSourceMap": true,
    // 为声明文件生成 sourceMap
    "declarationMap": true,
    // 声明文件目录 默认 node_modules/@types
    "typeRoots": [],
    // 声明文件包
    "types": [],
    
    // 删除注释
    "removeComments": true
    
    // 不输出任何文件
    "noEmit": false,
    // 发生错误时候不输出文件
    "noEmitOnError": true,
    
    // 不生成 helper 函数。需要额外安装 ts-helpers
    "noEmitHelpers": true,
    // 通过 tslib 引入 helper 函数,文件必须是模块
    // 这两个选项共同使用可以缩小编译后代码的体积
    "importHelpers": true,
    
    // 降级遍历的实现,针对 es3 | 5
    "downlevelIteration": true,
    
    // 开启所有严格的类型检查
    "strict": true,
    // 在代码中注入 'use Strict'
    "alwaysStrict": false,
    // 不允许隐式的 any 类型
    "noImplicitAny": false,
    // 不允许将 null 和 undefined 赋值给其他变量
    "stricNullChecks": false,
    // 不循序函数参数双向协变
    "strictFunctionTypes": false,
    // 类的实例属性必须初始化
    "strictPopertyInitialization": false,
    // 严格的 bind call apply 检查
    "strictBindCallApply": false,
    // 不允许 this 具有 any 类型
    "noImplicitThis": false,
    
    // 禁止声明但未使用的变量
    "noUnusedLocals": true,
    // 禁止声明但未使用的参数
    "noUnusedParameters": true,
}