Typescript全面介绍

1,896 阅读21分钟

前言

  Typescript作为ECMA的实现,javascript的超集,目前已经广泛在项目中使用。typescript是什么?有什么具体功能?这些已经被大家写得差不多了。在这里,我不再赘述ts的作用,而是直接用起来,从一个初学者角度告诉大家一些使用typescript的心得。下文将会完整地讲解一下typescript各个方面的具体用法,力求在我总结之余让大家有所收获。

正文

  typescript是type+script(js)。它的本质是通过类型定义来限制js灵活多变的语法。ts提供了一些基本类型,但同时允许开发者自定义类型,在一些复杂情况下还可以使用一些高级类型。在使用之前先总结几点可以帮助理解的要点:

  • ts代码可以由tsc(Type Script Complier)编译器编译成js代码,项目的ts代码无论在开发时还是打包后都会经历编译环节,最终执行的只会是js代码。
  • ts是js的超集,支持所有的JavaScript语法,支持es6、es7等新语法,不需要使用babel进行转换,在tsconfig.json配置文件中进行配置之后,tsc便能将其编译成指定版本的js代码。
  • ts可以通过tsconfig.json配置一些编译规则,违反这些规则是导致编译错误,但是即便编译错误不会打断编译,tsc还是会继续完成编译。

开始使用

  1. 全局安装typescript

    yarn global add typescript
    // 或使用 npm
    npm i typescript -g
    
  2. 使用tsc进行编译

    // 使用默认的配置编译helloword.ts文件
    tsc helloword.ts 
    
  3. 使用tsconfig.json配置文件

    // 在根目录下建立tsconfig.json配置文件
    {
        // 编译选项
        "compilerOptions": {
            // 编译目标,如'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
            "target": "es5",
            // 编译代码使用的模块 'commonjs', 'amd', 'system', 'umd' or 'es2015'
            "module": "commonjs",
            // 输出文件夹,默认会对应ts文件下生成同名js文件
            "outDir": "./dist",
            // 类型声明文件的目录,工作目录下@types文件会自动加入
            "typeRoots": [],
        },
        // 进行编译的文件
        // 包含在files中的文件是一定会进行编译(即使使用exclude去除)
        "files": [
            "hello.ts",
            "word.ts"
        ],
        // 要编译的目录或文件
        "include": [
            "./folder"
        ],
        // 不要包含的目录或文件
        "exclude": [
            "./folder/**/*.spec.ts",
            "./folder/someSubFolder"
        ]
    }
    

    完整的配置选项可以参考 typescript官网

    • 在建立好配置文件之后,直接执行tsc,编译好的js文件便会存在于js目录,当module为commonjs时,可以直接使用node进行执行测试。
    • 当然还可以使用自动编译,在tsconfig.json顶层中加入 "compileOnSave": true,告诉IDE保存后进行编译,vscode按照typescript插件,之后执行 tsc -w,这样子之后就能进行自动编译。

环境声明

  在写项目时经常会调用window的一些方法,比如window.addEventListener(),或者也会访问node的一些全局变量,如process.env,在IDE中使用这些的时候都能给我们提供代码提示,这极大的便利了我们。毫无疑问,这些全局变量已经定义了类型,那么这些定义存在哪里?又是谁帮我们定义了这些类型?

1.引入类型声明库

  在tsconfig.json配置中,{"compilerOptions": {"lib": []}},lib用于配置要引入的类型声明库,原生的lib有以下取值:

  • JavaScript 功能

    es5、es6、es2015、es7、es2016、es2017、esnext

  • 运行环境

    dom、dom.iterable、webworker、scripthost

  • ESNext 功能选项

    es2015.core、es2015.collection、es2015.generator、es2015.iterable、es2015.promise、es2015.proxy、es2015.reflect、es2015.symbol、es2015.symbol.wellknown、es2016.array.include、es2017.object、es2017.sharedmemory、esnext.asynciterable

当没有进行配置的时候:

当target为ES5时,lib为 DOM,ES5,ScriptHost
当target为ES6时,lib为 DOM,ES6,DOM.Iterable,ScriptHost

window的声明正是存在于dom库中。ts对那些新语法类型校验也是通过lib进行声明的,当需要使用一些比较新的es6语法时,可以在lib中加入对应的库,比如加入esnext以支持一些最新的语法。

有时候我们会将我们的全局变量或函数挂在window下,可惜的是在定义和使用的时候都会报错,解决这个问题的方法是利用的ts的类型合并,同名类型会自动合并。

declare interface Window {
  myDo(): boolean;
}

于是使用的时候便不会有错误提示,必须注意的是上面只是为Window这个类型增加了mydo方法,这个方法还是需要进行定义,不然运行时会报错。

//定义myDo方法
window.myDo = () => {
    console.log('Hello World');
    return true;
}
// 使用myDo
window.myDo()
  1. 使用@types

  当使用一些库的时候,你希望可以有类型提示,这时候可以安装使用@types,比如安装node的@types, npm install @types/node --save-dev

  • 默认情况下,TypeScript会自动包含支持全局使用的任何定义。在安装了jquery的@types之后就可以直接全局使用$
  • 安装@types默认加入会造成污染,这个时候可以按需引入,如下只会引入jquery的@types。
    {
      "compilerOptions": {
        "types" : [
          "jquery"
        ]
      }
    }
    
  1. 全局类型声明
  • 当一个文件没有使用模块,即没有使用es6的export等关键字,那么文件内的所有定义是全局的,也就是如果在一个没有使用模块的ts文件中定义的所有类型都将会是全局类型。

  • 通常的声明全局类型的做法是在一个.d.ts文件中定义所有的全局类型,该类型的文件并不会参与编译,在打包后仍然保留。在声明类型时所有的顶级类型都应该用declare进行声明。

    interface Global {
      env: { [key: string]: string };
    }
    // 声明一个全局变量
    declare let mGlobal: Global;
    
  • 在项目难免碰到没有类型声明的库,使用的时候有错误警告,这个时候就需要重写类型的动态查找。

    // global.d.ts
    declare module 'foo' {
      // some variable declarations
      export default any;
    }
    

    在导入foo模块的时候

    import foo from 'foo';
    // TypeScript 将假设(在没有做其他查找的情况下) foo 是 any
    

基本类型

  ts提供了Boolean、Number、String、Array、Tuple、Enum、Any、Void、Null、Undefined、Never、Object 共12种基本类型。

  1. Boolean、Number、String

     // 大部分类型的使用非常简单
    let bool: boolean = true;
    let num: number = 1213;
    let str: string = "Hello World";
    
  2. Array和Tuple(元组)

    // 数组类型
    let arr: number[] = [1213,2324]
    // 或者 
    const arr2: Array<string> = ['1213', '234234']
    
    // 元组为有限个有序的数组,元组需要对数组的每个元素声明类型
    let tup: [string, number] = ["121", 2321];
    tup[0] = "wadsas";
    tup[0] = 1212 // 报错
    
  3. Enum类型

      枚举类型是一种很特殊的类型,其它的类型声明在编译成js代码之后便不会存在,但枚举类型是会编辑成js代码的。

    enum Tristate {
      False,
      True,
      Unknown
    }
    // 编译成js之后
    var Tristate;
    (function(Tristate) {
      Tristate[(Tristate['False'] = 0)] = 'False';
      Tristate[(Tristate['True'] = 1)] = 'True';
      Tristate[(Tristate['Unknown'] = 2)] = 'Unknown';
    })(Tristate || (Tristate = {}));
    // 也就是以下对象
    {0: "False", 1: "True", 2: "Unknown", False: 0, True: 1, Unknown: 2}
    
    let tri: Tristate = Tristate.False
    

    枚举选项的值默认是从0开始,上面的Tristate中False为0, True为1,Unknown为2,我们可以改变枚举的值:

    enum HighColor {
      Green,
      Black = 5,
      Blue
    }
    // Green为0,Black为5,Blue为6
    // 字符串枚举
    enum Type {
      ON_LINE = "ON_LINE",
      DOWN = "DOWN",
      OTHER = "OTHER"
    }
    

    还可以使用 enum + namespace 的声明的方式向枚举类型添加静态方法

    enum PersonType {
      young,
      middle,
      old
    }
    namespace PersonType {
      export function isOld(person: PersonType) {
        return person === PersonType.old;
      }
    }
    const Marry: PersonType = PersonType.old;
    console.log(PersonType.isOld(Marry));
    
  4. Any和Object

    Any是任意类型,当一个变量被声明为了any类型,它可以是任何值,就如js一样可以随意改变其值而不会有提示,当从js项目迁移到ts,any可以提供极大的便利,但一个项目内应该尽量少地使用any类型,因为这意味着你放弃类型检查。

    let a: any = 1231;
    a = "12313";
    a = true
    

    Object是对象类型,它仅表示变量是对象类型,不会对对象的属性做出限制,一个比较适合的场景是Object.create(o: object)

    declare function create(o: object | null): void;
    create({ prop: 0 }); // OK
    create(null); // OK
    create(42); // Error
    
    let obj: object = {a: 12}
    obj = {b: 12, c: 88}
    obj.d = 1212 // 报错
    
    
  5. Void、Never、Null和Undefined

    Void一般用以表示函数或方法没有返回值,Never代表永远不会发生的类型。

    function log(name: string):void {
        console.log(name)
    }
    
    // 抛出了错误,程序永远没有返回值
    function error(message: string): never {
        throw new Error(message);
    }
    // 函数内有一个死循环,永远不会执行到循环外的代码
    function infiniteLoop(): never {
        while (true) {
        }
    }
    

    Null和Undefined类型为其它所有类型的子类型,这意味着该类型可以赋值为其它任何类型,但这必须在strictNullChecks编译选项为false的情况下。

    let n: null = null
    let u: undefined = undefined;
    let str: string = '1212'
    str = n
    
  6. 类型断言

      在需要将一个类型强制转换为另外一个类型时可以使用断言。之所以不叫类型转换是因为类型转换更多是运行时的,而ts断言则是编译时的判断。

    • 断言是有条件的:当 S 类型是 T 类型的子集,或者 T 类型是 S 类型的子集时,S 能被成功断言成 T。
    • 这是为了在进行类型断言时提供额外的安全性,完全毫无根据的断言是危险的,如果你想这么做,你可以使用 any。
    • 断言是有害的,它忽略了ts的提示,遵从了你的判断而不是编译器的。
    type Axis = {
      x: number;
      y: number;
    };
    let num = {};
    num.x = 123; // 报错 x不存在{}类型中
     num.y = 344;
    // 将num断言为Axis
    let num = {} as Axis;
    num.x = 1231;
    num.y = 789;
    
    // 双重断言,当两个类型没有子集关系时是不允许进行断言的,如果一定要断言,则必须使用双重断言
    let str = "12312"; // str被推断为string类型
    // str = 12312 报错,12312不是string类型
    // str = 12312 as string;  报错,number类型不能断言为string类型
    str = (12312 as any) as string;
    

自定义类型

  1. 内联方式

    let person: {name: string, age: number} = {name: '小龙女', age: 12}
    

    内联方式声明自定义类型较为直接,但其声明的类型无法复用。

  2. 声明函数

    函数可以直接使用function声明类型,如函数变量则可以直接通过内联方式声明。

    // 使用function进行声明
    function log(str: string): void {
      console.log(str);
    }
    
    // 对函数变量进行声明
    interface Log {
      (str: string): boolean;
    }
    type Log2 = {
      (str: string): boolean;
    }
    const myLog: Log = str => {
      console.log(str);
      return true;
    };
    // 直接使用箭头进行函数声明
    const toDo: (str: string) => void = str => {
      console.log(str);
    };
    
  3. 通过class声明类型

    es6新增的class可以直接声明类型,而且对于静态属性、静态函数的声明非常方便:

    class Cat {
      name: string;
      static Type: string;
    }
    
    // Cat.Type 会是string类型
    
    const cat: Cat = new Cat();
    
  4. interface和type

      interface和type大同小异,都可用于自定义类型,建议直接使用interface声明类型,在interface无法实现时使用type。

    interface Point {
      x: number;
      y: number;
      // 加?号表示可选
      x2?: number;
      // 声明方法
      computed?():void
    }
    // 同名接口合并,下面接口可以与上面的Point接口合并
    interface Point {
      x2: number;
    }
    // 接口继承
    interface Point_3D extends Point {
      z: number;
    }
    // 接口可以被class实现
    class ChartPoint implements Point {
      x: number;
      y: number;
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    }
    // 使用type声明一个类型
    type Person = {
        name: string;
        age: number;
        sex: string;
        showInfo(isLog: boolean):string;
    }
    // 为Point接口取个别名
    type Point2 = Point
    

    有一些情况下只能使用type进行自定义类型声明:

    interface Person {
      name: string;
      age: number;
      sex: string;
      id: string;
    }
    // SubPerson类型的所有属性都存在于Person类型中
    type SubPerson = {
      [key in keyof Person]: Person[key];
    };
    
    function createSubPerson(
        person: Person, 
        keys: Array<keyof Person>
    ): SubPerson {
      return keys.reduce(
        (obj, key) => ({ ...obj, [key]: person[key] }),
        {} as SubPerson
      );
    }
    
    

字面量类型

  我认为学习ts比较重要的是区分类型与变量,类型和变量是可以重名的,有些操作是针对类型,如keyof;有些则是针对变量, 如typeof;事实上分清类型与变量一点也不容易。在ts中,值也是一种类型——字面量类型(literal type)

// 所有的值都可以作为类型
// 字面量类型
let foo: "Hello"; // foo的值只能为Hello
foo = "Hello";
let num: 12;
num = 12;
let bool: true = true;
let obj2: { a: 121 } = { a: 121 };

变量只能为一个固定的值,这样看来字面量类型似乎没有什么价值?结合联合类型会让其大有所为:

type Strs = "a" | "b" | "c" | "d";
type Nums = 1 | 2 | 3 | 4 | 5;
type Bools = true | false;
let str:Strs = 'a' // str只能为a、b、c、d中的一个

// 对象和联合类型实现枚举类型的功能
// 枚举类型是一种奇特的类型,既可以充当类型,又可以当成对象进而访问它的属性

// strEnum将数组转为对象
function strEnum(o: Array<T>): { [K in T]: K } {
  return o.reduce((res, key) => {
    res[key] = key;
    return res;
  }, Object.create({}));
}
const OTHER = strEnum(["North", "South", "East", "West"]); //此处OTHER是对象
type OTHER = keyof typeof OTHER; // 此处OTHER为类型

// 现在OTHER具备了枚举类型的全部特性
let other2: OTHER;
other2 = OTHER.East;

类型推断

在刚开始使用ts的时候会出现这样的疑惑:并没有声明变量的类型,但变量再次赋值时报错:

let str = 'string'
str = 1212 //错误: Type '1212' is not assignable to type 'string'

这个是因为ts具备类型推断功能,ts会尽可能地找到变量的类型,下面是一些实践:

  1. 根据值推断类型

    // 1、定义变量
    let name = "string"; // 根据值推断name为string类型
    // name = 1212 //报错, number不能赋值为string类型
    
  2. 函数返回类型推断

    function returnNumber(a: number, b: number) {
      return a + b; // 可以推断到返回值是number类型
    }
    let num = returnNumber(12, 34); // num可以被推断到时number类型
    // num = '1231' // 报错
    console.log(num.toFixed()); // 调用number的toFixed方法没有问题
    

    函数的返回类型还是建议直接进行声明,因为在无法推断返回值的时候,返回的将是any类型,ts中应该尽可能减少any的出现。

  3. 函数的参数类型和返回值可以根据赋值来推断

    // doSomething这个变量声明为了函数类型
    const doSomething: (a: number, b: number) => number = (a, b) => {
      // a = "1312"; // 可推断到a和b为number类型。string赋值给number会报错
      return a + b;
    };
    
  4. 对象属性的推断

    const person = {
      name: "PerryHuang", // 可以推断name属性为string类型, age属性为number类型
      age: 24
    };
    person.name = 12312 // 报错 number不能赋值为string类型
    person.other = 121 // 报错, person没有other这个属性
    
  5. 解构变量的类型推断

    const person = {
      name: "PerryHuang", // 可以推断name属性为string类型, age属性为number类型
      age: 24
    };
    const { age } = person; // 可以推断出age为number类型
    console.log(age.toFixed(2));
    

    所以对于解构的变量的类型不应该去声明其本身,而是声明其解构对象,利用类型推断推断出解构变量的类型。

类型生成

 ts除了可以定义新的类型,还支持根据一个类型或变量生成另外一个类型。

  1. 复制类型
    如果想移动一个类:

    class Foo {}
    const Bar = Foo; // Bar仅是复制了Foo的变量声明空间,无法作为一个类型使用
    // const test: Bar = {}; // 报错,Bar会是一个value,不能作为type
    // 可以使用import解决上述问题
    namespace importing {
      export class Foo {}
    }
    import NewBar = importing.Foo; // NewBar也会是一种新类型
    const nb: NewBar = {};
    
  2. typeof根据变量生成类型
    typeof用于获取变量的类型

    let num = 123;
    let num2: typeof num = 345; // typeof num会得到number类型
    const obj = { a: 123, b: "string", c: { d: 12 }, e: [12, 45] }; 
    // typeof 取到obj的类型为
    // {a: number; b: string; c: { d: number;}; e: number[];}
    const obj2: typeof obj = { a: 456, b: "1231", c: { d: 789 }, e: [45, 67] };
    // 当然也可以只取obj某一个属性的类型
    const arr: typeof obj.e = [4, 5];
    

    须重视的是typeof的作用对象是变量,对类型使用typeof是没有作用的。还必须区分ts的typeof和js的typeof是不一样的,js的typeof是运行时,会返回一个表示类型的字符串。如typeof 12返回的是"number"

  3. keyof 根据类型生成类型
    keyof用于获取对象类型的所有建并生成一个联合类型:

    interface Person {
      name: string;
      age: number;
      isLive: boolean;
    }
    type Field = keyof Person; // Field 会是 'name' | 'age' | 'isLive'
    
    function getPersonField(person: Person, fieldName: Field) {
      return person[fieldName];
    }
    

    须重视的是keyof的作用对象是类型,对变量使用keyof是没有作用的。

  4. typeofkeyof组合使用

    const obj = {a: 12, b: true, c: 'asda'}
    type ObjKey = keyof typeof obj // 将会是'a' | 'b' | 'c' 联合类型
    const ok: ObjKey = "a";
    

    组合使用可以直接取到一个对象的所有属性并生成一个所有属性名的组成的联合类型。

ThisType

  js中方法内this指向其调用对象,也就是说方法中的this是在运行行才确定的。ts提供了ThisType用于this类型的控制。通过 ThisType 我们可以在对象字面量中键入 this,并提供通过上下文类型控制 this 类型的便捷方式。

在对象字面量方法中的 this 类型,将由以下决定:

  • 如果这个方法显式指定了 this 参数,那么 this 具有该参数的类型。
  • 否则,如果方法由带 this 参数的签名进行上下文键入,那么 this 具有该参数的类型。
  • 否则,如果 --noImplicitThis 选项(在配置文件中开启)已经启用,并且对象字面量中包含由 ThisType 键入的上下文类型,那么 this 的类型为 T。
  • 否则,如果 --noImplicitThis 选项已经启用,并且对象字面量中不包含由 ThisType 键入的上下文类型,那么 this 的类型为该上下文类型。
  • 否则,如果 --noImplicitThis 选项已经启用,this 具有该对象字面量的类型。
  • 否则,this 的类型为 any。
  1. 显式指定了this参数

    // his作为参数传入,则this的类型为参数的类型
    const obj = {
      x: 12,
      y: "232",
      print(this: { caller: { name: string } }) {
        this; // this为{ caller: { name: string } }
      }
    };
    
  2. 方法由带 this 参数的签名进行上下文键入

    // 没有传this参数,但开启了noImplicitThis, 方法由带 this 参数的签名进行上下文键入
    interface OBJ {
        x: number;
        y: string;
        print(x: number): void;
        f?(): void
    }
    const obj2: OBJ= {
      x: 12,
      y: "232",
      print(x: number) {
        this.x += x; // this的类型是obj2的类型
      }
    };
    obj2.f = function() {
      console.log(this.x); // this类型还是obj2的类型
    };
    
  3. ThisType指定方法的上下文

    // ThisType用以指定某个方法的上下文类型
    type ObjectDescriptor<D, M> = {
      data?: D;
      //  在methods对象里面的方法的上下文被指定为了 D & M 
      // D&M表示交叉类型,取D和M类型的并集
      methods?: M & ThisType<D & M>;
    };
    
    function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
      const data = desc.data || {};
      const methods = desc.methods || {};
      return { ...data, ...methods } as D & M;
    }
    
    const o = makeObject({
      data: {
        x: 12,
        y: 20
      },
      methods: {
        prindPoint() {
          // 可以直接访问到data中的变量,而不会给出错误提示
          console.log(this.x, this.y);
        },
        getPoint() {
          this.prindPoint();
          return [this.x, this.y];
        }
      }
    });
    

泛型

  泛型——用于在成员之间提供有意义的约束

  1. 泛型在类中的运用
    // 在这里使用原型约束了push、pop函数以及data数组,使它们的类型一致
    class Queue<T> {
      private data: T[] = [];
      push = (item: T) => this.data.push(item);
      pop = (): T | undefined => this.data.shift();
    }
    const queue = new Queue<number>();
    queue.push(12); // 只能push  number类型
    queue.push(18);
    console.log(queue.pop()?.toFixed(2)); // pop的返回值将会是number或undefined
    
  2. 泛型在函数中使用
    // 泛型在这里约束了函数的参数和返回值,它们的类型必须是一致的
    function reverse<T>(items: T[]): T[] {
      return items.reduce((result, item) => [item, ...result], []);
    }
    const arr = [12, 45, 23, 12, 54];
    console.log("reverse前:", arr);
    //根据reverse参数可以判定reversed现在是 number[]类型
    const reversed = reverse(arr); 
    // reversed[12] = "ad"; // 报错
    // reverse = ["12"]; // 报错
    console.log("reverse后:", reversed);
    
  3. 多个泛型类型
    function map<T, U>(arr: T[], fn: (arr: T) => U): U[] {
      return arr.map(fn);
    }
    const result = map<number, string>([12, 3453], item => item.toFixed(4));
    // result[1] = 1212; // 报错
    console.log("result:", result);
    

readonly

readonly用来使对象的属性只可读

  1. 基本使用

    //  1、通过内联注解使用readonly
    const hello: { readonly name: string; age: number } = {
      name: "Perry haung",
      age: 20
    };
    // hello.name = "2312" //报错,只读属性不可重写
    hello.age = 21; // 正常
    
    // 2、在interface或type中使用
    interface Person {
      readonly name: string;
      readonly age: number;
    }
    function printPerson(person: Person) {
      // person.name = 12 // 报错,不允许修改
      console.log(person);
    }
    printPerson({ name: "hello word", age: 12 });
    type Animal = {
      voice: string;
      readonly type: string;
    };
    
    // 3、class中使用readonly
    class Cat {
      readonly voice = "喵喵喵";
      readonly age: number;
      constructor() {
        // this.voice = "asda";不允许修改值
        this.age = 21; //可以进行第一次赋值
      }
    }
    
  2. Readonly用于将类型的所有属性转为只读

    interface TestType {
      name: string;
      age: number;
    }
    type TestType2 = {
      sex: number;
      id: string;
    };
    
    type RoTestType = Readonly<TestType>;
    type RoTestType2 = Readonly<TestType2>;
    
    let rtt: RoTestType = {
      name: "asdas",
      age: 888
    };
    
    // rtt.name = "asd"; // 不允许修改属性
    
  3. ReadonlyArray 只读数组

    type roArr = ReadonlyArray<number>;
    const arr: roArr = [12, 34, 213];
    // arr[1] = 231; //不允许修改
    
  4. 与const的对比

    • const用于变量,限制变量不能赋值给其它的值
    • readonly 用于对象属性,限制属性不能修改
    • readonly只能保证’我‘不修改属性,但当对象交给其它变量,而该变量没有做出限制的时候,它是可以修改的。
    interface HW {
     content: string;
      to: string;
    }
    const hw: Readonly<HW> = {
      content: "JS",
      to: "Perry Huang"
    };
    // hw.content = 'asdad' //hw这个变量是不能修改属性的
    function change(hw: HW) {
     //函数即便传入了只读属性对象的参数,
     // 因为这里参数声明的不是只读的,所以还是可以修改的
      hw.content = "Python"; 
     
    }
    change(hw);
    console.log(hw);
    

索引签名

  在js代码里,任何变量都可以作为索引,在其内部会隐式调用toString,在ts里则不允许这样,索引仅允许number和string,当对象变量作为索引时,必须显式调用toString。

let foo: any = {};
const obj = {
  toString() {
    return "hello";
  }
};
// foo[obj] = "Word"; // 报错,必须是string和number才能作为索引
foo[obj.toString()] = "Word";
  1. 声明一个索引签名

    nterface IndexSign {
      [key: string]: string; // 允许增加任意键为string值为string的类型
    }
    const bar: IndexSign = {};
    bar[obj.toString()] = "word";
    
  2. 同时使用string和number类型的索引签名

    interface ArrStr {
     // string类型索引的类型必须要包括number类型索引的值
      [key: string]: number | string;
      [index: number]: string;
    }
    const as: ArrStr = {
      1: "1",
      a: 1
    };
    as2[1] = "1213";
    
    
  3. 限制允许添加的属性

    type LimitKey = "name" | "id" | "sex";
    type LimitIndex = {
      [key in LimitKey]?: string;
    };
    const lk: LimitIndex = {};
    lk.id = "201541";
    lk.name = "PerryHuang";
    lk.sex = "man";
    

类型保护

ts具备一定的'智能',在一定的情景能推断出类型,这种功能叫做类型保护。

  1. typeof触发类型保护

    function doByType(type: number | string) {
      if (typeof type === "number") {
        // 根据typeof ts能判断当前type是number类型
        return type.toFixed(2);
      } else {
        // 结合上下文,ts确定当前的type是string类型
        return type.split("");
      }
    }
    console.log(doByType("Perry Huang"));
    console.log(doByType(12345));
    
  2. instanceof触发类型保护

    class A {
      a = "aaa";
      d = "ddd";
      commom = 888;
    }
    class B {
      b = "bbb";
      e = "eee";
      commom = 888;
    }
    function testInstanceof(obj: A | B) {
      if (obj instanceof A) {
        console.log(obj.a); // 识别obj为类A的实例,可以访问a属性
      } else {
        console.log(obj.b); // 识别obj为类B的实例,可以访问b属性
      }
    }
    testInstanceof(new A());
    testInstanceof(new B());
    
  3. in 判断触发类型保护

    function testIn(obj: A | B) {
      if ("a" in obj) {
        console.log(obj.d); // 识别obj为类A的实例,可以访问d属性
      } else {
        console.log(obj.e); // 识别obj为类B的实例,可以访问e属性
      }
    }
    testIn(new A());
    testIn(new B());
    
  4. 字面量类型保护

    type Foo = {
      type: 1;
      foo: string;
    };
    
    type Bar = {
      type: 2;
      bar: string;
    };
    
    function testLiteral(param: Foo | Bar) {
      if (param.type === 1) {
        console.log(param.foo); // 识别param为Foo类型,可以访问foo属性
      } else {
        console.log(param.bar); // 识别param为Bar类型,可以访问bar属性
      }
    }
    
    testLiteral({ type: 1, foo: "I am Foo type." });
    testLiteral({ type: 2, bar: "I am Bar type." });
    
  5. 自定义类型保护

    function isFoo(arg: Foo | Bar): arg is Foo {
      return (arg as Foo).foo !== undefined;
    }
    function testCustom(param: Foo | Bar) {
      if (isFoo(param)) {
        console.log(param.foo); // 识别param为Foo类型,可以访问foo属性
      } else {
        console.log(param.bar); // 识别param为Bar类型,可以访问bar属性
      }
    }
    testCustom({ type: 1, foo: "I am Foo type." });
    testCustom({ type: 2, bar: "I am Bar type." });
    

结语

  本文到此已经结束了,总的来说,ts是对js的限制,限制了自由赋值,函数的参数、返回值等。加入了ts的限制,能让代码更加规范,易于维护,易于重构。本文没有深入去讲解这些类型的实现原理,更多停留在’用‘的层面,有不足之处请大家指点与谅解。

参考书籍

深入理解Typescript