TypeScript由浅入深

356 阅读10分钟

TypeScript 是什么

静态弱类型语言
静态: 在编译阶段就检查每个变量的类型。(相反地,在运行过程中才检查数据类型的语言为动态语言)
弱类型: 支持隐式类型转换。(完全兼容 js,可以把 string 类型的变量赋值给 bool 类型)

如何理解 TypeScript 的存在意义

TypeScript 其实是在 js 的基础上创造了另一个平行世界-类型声明空间,拥有两个空间:类型声明空间和变量声明空间。
ts1.jpeg

两个空间是两个世界

  • 类型在类型空间里可以相互引用赋值,但不能当变量用
  • 变量在变量空间里可以相互引用赋值,但不能当类型用
  • 两个空间的声明可以同名

但是两者又有交流

  • 在变量声明空间中,可以通过一些语法给变量声明类型,如 const 变量: 类型
  • 在类型声明空间中,可以通过一些语法捕获变量的类型,比如 type 类型: typeof 变量

在 TS 两个空间,这么多声明语法中:

  • 有些声明方式只声明变量,比如const a = 1;
  • 有些声明方式只声明类型,比如interface A {};
  • 有些声明方式会同时声明类型和变量,比如 class A {}
  • 有些声明方式再声明类型的同时,还同时创建了变量空间的数据结构,比如 enum
class A {} // 在类型空间声明了A类型,在变量空间声明了A变量
interface B {} // 在类型空间声明B类型
const C = 1; // 在变量空间声明了C变量

let typeB: B; // B可以当做注解使用
let varB = B; // 'B' only refers to a type, but is being used as a value here.
// B不能当变量使用

let typeC: C; // 'C' refers to a value, but is being used as a type here. Did you mean 'typeof C'?
// C不能当注解使用
let varC = C; // C可以当做变量使用

let varA = A; // A可以当注解使用
let typeA = A; // A可以当做类型使用

declare 环境声明

通常 TS 中,声明变量的同时会注解类型,例如const name:string = '小明'; 。但是如果引入一些没有 ts 声明的第三方包,会造成类型声明空间不存在变量声明空间中变量对应的类型。比如引入 jQuery 之后,变量声明空间存在变量$,但是类型声明空间并没有变量$对应的类型声明。这个时候需要使用 declare 关键字,来告诉 TypeScript,我正在试图补充声明一个其他地方已经存在的变量$的类型。declare const $: any
declare 用来定义已经声明的变量的类型,表明这句 ts 只用于类型声明空间变量声明空间中已存在内容的类型补充,不具有执行意义

TypeScript 基础类型--类型声明空间的万物之源

类型声明空间的基础类型,就像变量声明空间的 1,'a',true...。在类型声明空间可以直接使用基础类型,也可以通过声明、运算、派生来构造、流转新的类型。

const name:string='Icey'; // 直接使用
type bar = number; // 赋值给类型声明

[类型声明空间]基础类型有:

  • 数字 number

  • 字符串 string

  • 布尔值 boolean

  • symbol

  • null

  • undefined

  • 数组

  • 元组
    其实是个数组,只是比数组更精细,会声明长度和各元素类型。const tuple:[number,string] = [1,'1']

  • 枚举
    enum 是 ts 创造的一种声明,表示枚举,enum 不仅在类型空间产生声明,也会在变量空间构建特殊结构。

    
    enum Color {
    Red,
    Green,
    Blue,
    }
    
    // 编译后如下
    var Color;
    (function (Color) {
      Color[(Color['Red'] = 0)] = 'Red';
      Color[(Color['Green'] = 1)] = 'Green';
      Color[(Color['Blue'] = 2)] = 'Blue';
    })(Color || (Color = {}));
    

    枚举类型提供的一个便利是可以由枚举的值得到它的名字,但仅限于值是数字的情况

  • any

  • void: 表示没有返回值

  • never
    never 是 TypeScript 的底层类型,表示那些永不存在的值的类型

  • object

TypeScript 类型声明的方式和区别

基础类型可以直接拿来使用,有时我们会在基础类型的基础上,进行类型的别名、组合和复杂类型的创建,这种操作就是类型声明,主要通过一些关键字来实现

  • type

  • interface
    interface 规定了代码中数据实现需满足的契约。
    type 和 interface 很像,都可以用来声明对象属性、函数等,都是纯类型声明,只在类型声明空间产生声明,编译之后就不存在了。但是 interface 可以进行 merge,type 不可以;type 可以用于类型捕获(typeof)和联合类型,interface 不可以。详见Interfaces vs Types in TypeScript

  • class
    class 本来是 js 的语法,当 class 用在 ts 中,它会保留变量空间中的 class 声明,同时在类型空间声明一个类型。

  • namespace
    namespace 主要解决命名冲突的问题,会生成一个对象,定义在 namespace 内部的类型都要通过这个对象访问。namespace 是注册到全局的,可以跨文件共同维护一个同名 namespace

  • module module就是模块,一个文件即是一个模块。 在 TS1.5 之前,没有 namespace 关键词,命名空间也是通过 module 来实现。但因为 js 本身就有 module 的概念,而且也是 ES6 的关键字,各种加载框架比如 CommonJS,AMD 等也都有 module 的概念,但是 TS 之前的 module 关键字与他们都不太相同,所以在 TS1.5 之后换了 namespace 关键字来表示命名空间,避免造成概念上的混淆。然后推荐代码中不要再出现 module 关键字,这个关键字基本变成了一个编译后和运行时的概念,留给纯 js 中使用。 现在 module 多用于外部模块,通常只在用于补充声明外部 js 模块时看到

    declare module Hello {
      export const name:string;
      export const sayHi:()=>void;
    }
    

    带 export 在 module 或 namespace 上,使用时就需要 import,不带 export 就不需要 import,可以全局访问。
    module 和 namespace 里面的东西需要暴露也需要各自 export。例如 export module A { export class B}

TypeScript 类型的运算和派生

  • 联合类型
    type Union = number | string

  • 字面量类型
    type Name = 'Icey' | 'Rose'

  • 联合类型和类型保护

  • 交叉类型

    interface PersonInfo {
    name:'Icey',
    age:28
    }
    interface WorkInfo {
    job: 'web',
    salary: null
    }
    type Info = PersonInfo & WorkInfo;
    
  • 泛型(interface 中的泛型、class 中的泛型) 泛型是指在函数签名时不预先指定具体的类型,而是在使用的时候再捕获传入的类型,常用于约束参数类型和函数返回值类型之间的关系。

    泛型在 interface 和 type 中使用

    interface ResponseData<DataType> {
    status: number;
    data: DataType;
    }
    
    type Pick<T,K extends keyof T> = {
    [P in K]: T[P];
    };
    

    泛型在 class 和函数中使用

    class Queue<T> {
    private data: T[] = [];
    push = (item: T) => this.data.push(item);
    pop = (): T | undefined => this.data.pop();
    }
    const queue = new Queue<string>(); // 此处需传入泛型类型,否则T为unknown
    queue.push("aa");
    queue.push(11); // Error:Argument of type 'number' is not assignable to parameter of type 'string'.
    
    const getQueue = <T>(firstItem: T): T[] => {
    return [firstItem];
    };
    
    getQueue(1); // 可不传泛型类型,ts 有类型推论可得出 T 为 number
    
    
  • 索引类型
    keyof 索引查询操作符:对于任何类型 T,keyof T 的结果为该类型上所有公有属性 key 的联合

    interface Example1 {
     name: string;
     readonly age: number;
    }
    type T1 = keyof Example1; // type T1 = 'name' | 'age'
    class Example2 {
      private name: string;
      public readonly age: number;
      protected home: string;
      }
    type T2 = keyof Example2; // type T2 = 'age'
    

    T[K]索引访问操作符

    type T3 = Example1["name"]; // type T3 = string
    type T4 = Example1[keyof Example1]; // type T4 = string | number
    
  • 映射类型
    keyof 操作符将接口的索引归纳到一个类型上。相反地,我们可以再将一个类型映射回到索引上

    type T6 = "age" | "name";
    type T7 = {
    [P in T6]: string;
    }; // 这里只能根据联合类型映射出一个Type,不能是interface,会报错,如果需要映射一个interface,如下操作
    interface Example3 extends T7 {}
    

    Tips: extends 关键字有两种用法

    1. 表示继承,多用于 interface、type、class 中。注意继承多个 interface 时, interface 之间用逗号隔开;继承多个 type 时,type 之间用&连接为交叉类型。
    2. 表示约束类型,多用于泛型中,用 T extends xxx,限制 T 只能为 xxx 类型。
    type NoNullable<T extends Record<any, any>> = {
    [P in keyof T as T[P] extends null | undefined ? never : P]: T[P];
    };
    type demo1 {
      name: string;
      age: number;
      boyFriend: undefined;
    }
    let demo2: demo1 =  {
      name: "Icey",
      age: 28,
    } // ❌:Property 'boyFriend' is missing in ....
    
    let demo3: NoNullable<demo1> = {
      name: "Icey",
      age: 28,
    };// ✅
    
    

    在上例中,T extends Record<any,any>限制 T 只能传入索引类型;T[P] extends null|undefined?never:P,限制 T[P]为 null|undefined 类型,若为 null|undefined,则返回 never,代表删除属性,否则返回原类型。

    在 ts 的工具类型的实现中,比较多用类型映射,比如 Partical

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

理解 ts 中的父子类型及其关系

ts 中子类型可以赋值给父类型,反之不行。子类型更具体,父类型约束相对较少更宽泛。

interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}
let a: Animal;
let b: Dog;
a = b;
b = a; // Property 'break' is missing in type 'Animal' but required in type 'Dog'.ts(2741)

协变和逆变(Covariance and Contravariance)

引用维基百科定义

协变与逆变(Covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

简单理解就是原来具有父子关系的多个类型,在通过某种构造关系构造成新的类型,新类型如果还具有一样的父子关系则是协变,如果关系逆转(父变子,子变父)就是逆变。

interface Animal {
  name: string;
}

interface Dog extends Animal {
  break(): void;
}

type Covariant<T> = T[];

let animalArr: Covariant<Animal> = [];
let dogArr: Covariant<Dog> = [];
animalArr = dogArr; //✅ 在Animal和Dog变成数组后,Array<Dog>依然可以赋值给Array<Animal>,因此对于type Covariant<T> = T[]来说就是协变

type Contravariant<T> = (p: T) => void;
let animalFn: Contravariant<Animal> = function (p) {};
let dogFn: Contravariant<Dog> = function (p) {
  p.break();
};

animalFn = dogFn; // ❌ animalFn=dogFn不可以再赋值了
dogFn = animalFn; // ✅ dogFn = animalFn可以赋值,DogFn变为了父类型,AnimalFn变为了子类型,因此对于type Contravariant<T> = (p:T)=>void来说就是逆变

为什么 ts 认为 AnimalFn = DogFn 赋值不安全?
从例子可以看到,如果 dog 函数赋值给 animal 函数,那么 animal 函数在调用时,约束的参数为 Animal 类型不是 Dog 类型,但是 animal 调用实际为 dog 的调用,此时就会出现错误。因此 Animal 和 Dog 在进行 type Contravariant = (p:T)=>void 构造器构造之后,AnimalFn 不再兼容 DogFn

双向协变

在 ts 中,为了灵活性等权衡,对于函数参数默认的处理是双向协变,也就是类型 T 和通过构造器构造的类型 Com互相兼容。这一行为可以通过 tsconfig 中 strictFunctionType 来控制严格按照逆变来约束赋值关系

type Bivariant<T> = (p: T) => void;

let animalBi: Bivariant<Animal> = function (p) {};
let dogBi: Bivariant<Dog> = function (p) {};

animalBi = dogBi; // ✅ animalBi = dogBi可以赋值
dogBi = animalBi; // ✅ dogBi = animalBi也可以赋值

以上内容仅是个人的学习总结与记录,如有错误欢迎大家批评指正,注明参考文档,特此感谢这些优秀作者的内容分享

Typescipt 官方手册指南
浅谈 Typescript(二):基础类型和类型的声明、运算、派生
22 个示例深入讲解 Ts 最晦涩难懂的高级类型工具
聊聊 TypeScript 类型兼容,协变、逆变、双向协变以及不变性
区分 TS 中的 namespace 和 module
TypeScript Playground网页