前端追梦人TypeScript教程

一. TypeScript的存在价值?

  1. JavaScript提供类型系统,弥补其弱类型导致的问题
  2. TypeScriptJavaScript的超集,兼容所有JavaScript目前及未来所有的特性
  3. 编辑器基于类型系统可以给予开发者更多的智能提示
  4. 类型系统有利于提高代码的质量和可维护性,有利于代码重构
  5. 在编译期间捕获错误,避免很多以往在运行期才能发现的错误
  6. 可以在代码层面提供良好的文档
  7. TypeScript书写的代码最终会被编译成JavaScript代码
  8. Flow等类型检查系统相比,TypeScript的优势在于JavaScriptTypeScript

TypeScript只是带有文档的JavaScript, TypeScript让JavaScript更美好, 学习JavaScript仍然是必要的

二. TypeScript支持的ES6语法

  1. class
  2. 箭头函数
  3. rest参数
  4. let
  5. const
  6. 解构赋值
  7. 扩展运算符
  8. for…of
  9. 迭代iterator
  10. 模板字符串**``**
  11. Promise
  12. generators
  13. async/await

三. TypeScript项目构成

3.1 编译上下文

  • 通过tsc --init可以生成默认的配置文件
  • 使用tsconfig.json里通过compilerOptions来指定TypeScript的编译选项
{
  "compilerOptions": {
    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    // "outDir": "./",                        /* Redirect output structure to the directory. */
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    // "noUnusedLocals": true,                /* Report errors on unused locals. */
    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */

    /* Advanced Options */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}

3.2 声明空间

3.2.1 类型声明空间

类型声明空间包含用来当做类型注解的内容,它被用作 类型注解 使用,不能当作变量使用

class Foo {}
interface Bar {}
type Bas = {};

// 可以当作类型注解使用:
let foo: Foo;
let bar: Bar;
let bas: Bas;

// 不能够把它作为一个变量来使用
interface Bar {}
const bar = Bar; // Error: "cannot find name 'Bar'"

3.2.2 变量声明空间

变量声明空间包含可以用作变量的内容,class Foo提供了一个类型Foo到类型声明空间,此外还提供了一个变量到Foo到变量声明空间,因此下列语句可以正常通过TypeScript类型检查

const foo = Foo

但是我们不能把类似于 interface 定义的内容当作变量使用,同时变量也不能用作类型注解

const foo = 123;
let bar: foo; // ERROR: "cannot find name 'foo'"

3.3 模块

3.3.1 全局模块

默认情况下,当你在一个新的TypeScript文件中写下代码时,它处于全局命名空间中。如在foo.ts里的以下代码:

const foo = 123;

如果你在相同的项目里创建了一个新的文件bar.ts,TypeScript类型系统将会允许你使用变量foo,就好像它在全局可用一样:

const bar = foo; // allowed

毋庸置疑,使用全局变量空间是危险的,因为它会与文件内的代码命名冲突。我们强烈推荐使用文件模块。

3.3.2 文件模块

它也被称为外部模块。如果在你的TypeScript文件的根级别位置含有import或者export,它会在这个文件中创建一个本地的作用域。因此,我们需要把上文foo.ts改成如下方式

export const foo = 1

3.4 命名空间

防止变量泄露到全局, TypeScript提供了namespace关键字来描述这种分组

export namespace Utils {
  export function log(msg: any) {
    console.log(msg);
  }
}

Utils.log('hello')

namespace也是支持嵌套的

四. 类型系统

4.1 基本概念

  1. 基本注解
   const num: number = 123
   function test(arg: string): void {
       console.log(arg)
   }

上面代码使用了变量注解,函数参数注解和函数返回值注解

  1. 基本类型注解
    JavaScript的基本数据类型适用于TypeScript的类型系统
   const str: string = 'hello'
   const boo: boolean = true
   const num: number = 123
   const ss: Symbol = Symbol('123')
   const nn: undefined = undefined
   const nn1: null = null
  1. 数组注解
let array1:Array<number> = new Array<number>();
let array2:number[] = [123];
  1. 接口注解
    接口说明一种数据结构,是TypeScript的一个核心
interface People {
       name: string,
       age: number,
       hobby?: string
   }
   let p: People = {
       name: 'david',
       age: 23
   }
  1. 内联类型注解
   let p: {
       name: 'david',
       age: 23
   }
   
   p = {
       name: 'david'
   }
  1. 特殊类型注解(any, null, undefined, void)
  • any
    能够接收所有的类型(包括any类型),因此所有类型的数据都可以赋值给any类型的变量
     let aa: any
     
     aa = 123
     
     aa = 'hello'
     
     aa =  true

当使用any的使用意味着告诉编辑器不要做任何类型检查,要尽量减少any的使用

  • nullundefined
    let num = null
    num = undefined
  • void
    用来表示一个函数没有返回值
    const hello = (): void => {
        console.log('hello')
    }
    hello()
  1. 泛型
import { Utils } from "./utils";

// 1. 函数泛型
export function getIndexChild<T>(arr: T[], index: number): T {
  return arr[index];
}

Utils.logAnything(getIndexChild(["a", "b", "c"], 1));
Utils.logAnything(getIndexChild([1, 2, 3], 2));

// 2. 泛型类
class Counter<T> {
  a: any;
  b: any;
  constructor(a: T, b: T) {
    this.a = a;
    this.b = b;
  }
  getSum() {
    if (typeof this.a === "number" && typeof this.b === "number") {
      return this.a + this.b;
    } else if (typeof this.a === "string" && typeof this.b === "string") {
      return this.a.length + this.b.length;
    }
  }
}

const c1 = new Counter<number>(1, 2);
Utils.logAnything(c1.getSum());
const c2 = new Counter<string>("hello", "world");
Utils.logAnything(c2.getSum());

// 3. 泛型函数接口
export interface multiplyFun {
  <T>(arr1: T[], arr2: T[]): T[];
}

const concat: multiplyFun = (arr1, arr2) => {
  return arr1.concat(arr2);
};

Utils.logAnything(concat([1, 2, 3], [4, 5, 6]));

// 4. 泛型类接口
export interface Param {
  [index: string]: any;
}

class Page {
  private currentPage: number = 1; //当前页码 默认1
  private pageSize: number = 10; //每页条数 默认为10
  private sortName: string; //排序字段
  private sortOrder: string = "asc"; // 排序规则 asc | desc 默认为asc正序

  constructor(param: Param) {
    if (param["currentPage"]) {
      this.currentPage = param["currentPage"];
    }
    if (param["pageSize"]) {
      this.pageSize = param["pageSize"];
    }
    if (param["sortName"]) {
      this.sortName = param["sortName"];
    }
    if (param["sortOrder"]) {
      this.sortOrder = param["sortOrder"];
    }
  }

  public getStartNum(): number {
    return (this.currentPage - 1) * this.pageSize;
  }
}

interface User {
  id: number; //id主键自增
  name: string; //姓名
  sex: number; //性别 1男 2女
  age: number; //年龄
  city: string; //城市
  describe: string; //描述
}

//泛型接口
interface BaseDao<T> {
  findById(id: number): T; //根据主键id查询一个实体
  findPageList(param: Param, page: Page): T[]; //查询分页列表
  findPageCount(param: Param): number; //查询分页count
  save(o: T): void; //保存一个实体
  update(o: T): void; //更新一个实体
  deleteById(id: number); //删除一个实体
}

class UserDao<User> implements BaseDao<User> {
  findById(id: number): User {
    throw new Error("Method not implemented.");
  }
  findPageList(param: Param, page: Page): User[] {
    throw new Error("Method not implemented.");
  }
  findPageCount(param: Param): number {
    throw new Error("Method not implemented.");
  }
  save(o: User): void {
    throw new Error("Method not implemented.");
  }
  update(o: User): void {
    throw new Error("Method not implemented.");
  }
  deleteById(id: number) {
    throw new Error("Method not implemented.");
  }
}

  1. 联合类型注解
      const format = (word: string[] | string): string => {
        let line = "";
        if (typeof word === "string") {
          line = word;
        } else {
          line = word.join("").trim();
        }
        return line;
      };
      
      console.log(format(["a", "b", "c"]));
      
  1. 交叉类型
      const extend = <T, U>(first: T, second: U): T & U => {
        const result = <T & U>{};
        for (const id in first) {
          (<T>result)[id] = first[id];
        }
        for (const id in second) {
          // @ts-ignore
          if (!result.hasOwnProperty(id)) {
            (<U>result)[id] = second[id];
          }
        }
        return result;
      };
      const x = extend({ a: "hello" }, { b: "world" });
      console.log(x)
  1. 元组类型
    数组中元素为不同类型
       let arr: [number, string] = [0, '']
       arr[0] = 123
       arr[1] = 'hello'
       console.log(arr)
  1. 类型别名
      export type strOrNum = string | number;
      export type Text = string | { text: string }
      export type Coordinates = [number, number]
      export type Callback = (data: string) => void

4.2. 迁移JavaScript代码至TypeScript

一般来说,将JavaScript代码迁移至TypeScript包括以下步骤:

  • 添加一个tsconfig.json文件。

  • 把文件扩展名从.js改成.ts,开始使用any来减少错误。

  • 开始在TypeScript中写代码,尽可能减少any的使用。

  • 回到旧代码,开始添加类型注解,并修复已识别的错误。

  • 为第三方JavaScript代码定义环境声明。(很多三方库的类型声明已经发布到了DefinitelyTyped中)

  • TypeScript中可以允许导入任何文件,例如.css文件, 使用webpack的话只需要在globals.d.ts中添加如下代码 即可

    declare module '*.css'
    

    如果想使用HTML模板,如在Angular, 可以这样做

    declare module '*.html'
    

4.3. @types

毫无疑问,类型是TypeScript最大的优势之一,社区已经记录了将近90%的顶级JavaScript项目。这意味着,我们可以以交互式和探索性的方式来使用这些项目

4.3.1 使用@types

yarn add @types/jquery --save-dev

@types支持全局和模块类型定义

4.3.2 全局@types

在默认情况下,TypeScript会自动包含支持全局使用的任何定义。例如,对于jQuery,你应该能够在项目中全局使用$。

4.3.3 模块@types

对于jQuery来说,通常建议使用模块。安装模块@types之后,不需要进行特别的配置,你就可以像使用模块一样使用它了。

import * as $ from 'jquery'
// 现在可以在次模块中任意使用$了

4.3.4 控制全局类型泄露

可以在tsconfig.json的compilerOptions.types选项,引入有意义的类型

{
  "compilerOptions": {
    "types": [
      "jquery"
    ]
  }
}

如上例所示,在配置compilerOptions.types:[“jquery”]之后,只允许使用jQuery的@types包。即使安装了另一个声明文件,如npm install@types/node,它的全局变量(如process)也不会泄漏到你的代码中,直到你将它们添加到tsconfig.json类型选项中。

4.4. 环境声明文件

你可以选择把这些声明放入**.ts.d.ts里。在实际的项目中,强烈建议把声明放入独立的.d.ts里,你可以从一个命名为globals.d.tsvendor.d.ts**的文件开始。

如果一个文件有扩展名**.d.ts**,这意味着每个根级别的声明都必须以declare关键字作为前缀。这可以让开发者清楚地知道,在这里,TypeScript不会把它编译成任何代码,同时,开发者需要确保所声明的内容在编译时存在。

声明文件编写参考

4.5 接口

// declare const myPoint: { x: number; y: number };

interface Point {
    x: number;
    y: number;
}

declare const myPoint: Point

使用内联方式的类型定义和使用接口的方式定义是等效的,但是使用接口的方式便于其他使用者对其进行扩展

如在a.d.ts中有以下代码

interface Point {
    x: number;
    y: number;
}

在b.d.ts中有如下代码对接口Point进行了扩展

interface Point {
    z: number;
}

4.5.1 类可以实现接口

如果你希望在类中使用必须要被遵循的接口(类)或别人定义的对象结构,可以使用implements关键字来确保其兼容性

基本上,在implements存在的情况下,外部Point接口的任何更改都将导致代码库中的编译错误,因此可以轻松地使其保持同步

class MyPoint implements Point {
    x!: number;
    y!: number;
    z!: number;
}

如果接口Point的结构发生了变化,自然MyPoint也需要相应作出改变,否则就会报错

4.6 枚举

4.6.1 数字枚举

enum Color {
    RED = 100,
    GREEN = 200,
    YELLOW = 300
}

console.log(Color.RED)
console.log(Color[100])

上面代码console.log的结果是因为上面的ts代码被编译后生成的js代码是这样的

var Color;
(function (Color) {
    Color[Color["RED"] = 100] = "RED";
    Color[Color["GREEN"] = 200] = "GREEN";
    Color[Color["YELLOW"] = 300] = "YELLOW";
})(Color || (Color = {}));
console.log(Color.RED);
console.log(Color[100]);

Color[Color["RED"] = 100] = "RED";
这句代码会执行下述过程

Color["RED"] = 100 并返回 100 (在JavaScript中,赋值运算符返回的值是被赋予的值)
Color[100] = "RED"

用数字枚举做标记的一个实例

enum AnimalFlags {
  None = 0,
  HasClaws = 1 << 0,	// 0000 0000
  CanFly = 1 << 1,    // 0000 0010
  EatsFish = 1 << 2,  // 0000 0100
  FlyingClawedFishEating = HasClaws | CanFly | EatsFish  // 0000 0001 || 0000 0010 || 0000 0100 0000 0111 -> 7
}

interface Animal {
  flags: AnimalFlags;
  [key: string]: any;
}

const printAnimalAbilities = (animal: Animal): void => {
  var animalFlags = animal.flags;
  if (animalFlags && animalFlags === AnimalFlags.HasClaws) {
    console.log("有爪子的动物 ");
  } else if (animalFlags && animalFlags === AnimalFlags.CanFly) {
    console.log("会飞的动物");
  } else if (animalFlags && animalFlags === AnimalFlags.EatsFish) {
    console.log("吃鱼的动物");
  } else if (
    animalFlags &&
    animalFlags === AnimalFlags.FlyingClawedFishEating
  ) {
    console.log("万能的动物");
  } else if (animalFlags == AnimalFlags.None) {
    console.log("什么也不会的动物");
  }
};

var animal: Animal = { flags: AnimalFlags.None };   // 0000 0000
animal.flags |= AnimalFlags.HasClaws    //  0000 0000  || 0000 0001
animal.flags &= ~AnimalFlags.HasClaws   // 0000 0001 & 1111 1110  0000 0000
animal.flags |= AnimalFlags.FlyingClawedFishEating      // 0000 0000 || 0000 0111      
printAnimalAbilities(animal);

4.6.2 字符串枚举

enum MessageType {
    SUCCESS = 'success',
    ERROR = 'error',
    WARN = 'warning'
}

console.log(MessageType.SUCCESS)

4.6.3 常量枚举

有时定义枚举可能只是为了让程序可读性更好,而不需要编译后的代码,即不需要编译成对象。typescript中考虑到这种情况,所以加入了 const enum

const enum Tristate {
    False,
    True,
    Unknown
}
const lie = Tristate.False;

最终编译的js代码如下,并不会为枚举Tristate生成对应的Js代码

"use strict";
var lie = 0 /* False */;
//# sourceMappingURL=ConstEnum.js.map

如果需要让常量枚举也能编译到最终生成的JS代码中,可以使用preserveConstEnums编译选项

4.6.4 有静态方法的常量枚举

可以使用enum+namespace的声明方式向枚举类型添加静态方法。如下面的例子所示,我们将静态成员isBusinessDay添加到枚举上。

enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

namespace Weekday {
    export function isBusinessDay(day: Weekday){
        switch (day) {
            case Weekday.Saturday:
            case Weekday.Sunday:
                return false;
            default:
                return true;
        }
    }
}

const mon = Weekday.Monday;
const sun = Weekday.Sunday;

console.log(Weekday.isBusinessDay(mon))
console.log(Weekday.isBusinessDay(sun))

4.7 lib.d.ts

当你安装TypeScript时,会顺带安装一个lib.d.ts声明文件。这个文件包含JavaScript运行时及DOM(Document Object Model,文档对象模型)中存在的各种常见的JavaScript环境声明。
你可以通过指定**–noLib的编译器命令行标记,或者在tsconfig.json中指定选项noLib:true**,从上下文中排除此文件。

lib.d.ts的内容主要是一些变量声明,如windowdocumentmath等,以及一些类似的接口声明,如WindowDocumentMath。阅读全局内容的文档和类型注解的最简单的方法是输入你知道有效的代码,如Math.floor,然后在IDE上按F12键,跳转到定义上。

4.7.1 --lib选项

有时,你想要解耦编译目标(即生成的JavaScript版本)和环境库支持之间的关系。例如,对于Promise,你的编译目标是–target es5,但是你仍然想使用Promise,这时,你可以使用–lib选项来显示地控制lib。
注意:使用–lib选项可以将任何lib与–target解耦。

命令行

tsc --target es5 --lib dom,es6

tsconfig.json:

"compilerOptions": {
  "lib": ["dom", "es6"]
}

4.7.2 lib的分类

● JavaScript功能

  1. es5
  2. es6
  3. es2015
  4. es7
  5. es2016
  6. es2017
  7. esnext
  • 运行环境

    1. dom
    2. dom.iterable
    3. webworker
    4. scripthost
  • ESNext功能选项

    1. es2015.core
    2. es2015.collection
    3. es2015.generator
    4. es2015.iterable
    5. es2015.promise
    6. es2015.proxy
    7. es2015.reflect
    8. es2015.symbol
    9. es2015.symbol.wellknown
    10. es2016.array.include
    11. es2017.object
    12. es2017.sharedmemory
    13. esnext.asynciterable

–lib选项提供了非常高效的控制,因此你最有可能从运行环境与JavaScript功能类别中分别选择一项。如果没有指定–lib,则会导入默认库。

  • 当–target选项为es5时,会导入es5、dom、scripthost。
  • 当–target选项为es6时,会导入es6、dom、dom.iterable、scripthost

4.8 函数

函数类型在TypeScript类型系统中扮演着非常重要的角色,它们是可组合系统的核心构建块。

4.8.1 参数注解

function func(arg: string| string[] | undefined | null) {
    // do something
}

4.8.2 函数返回值注解

function func(arg: string| string[] | undefined | null): Result {
    // do something
    return {
        data: [],
        type: 'success'
    }
}

interface Result {
    data: any,
    type: string
}

4.8.3 可选参数, 参数默认值

function func(arg: string| string[] | undefined | null, canBeNull?: number = 1): Result {}

4.8.4 函数重载

// 函数重载
function print(a: number): void;
function print(a: number, b: number): void;
function print(a: number, b: number, c:number): void;
function print(a?: number, b?:number, c?: number): void {
    if(!a && !b && !c ) return
    if(a && !b && !c) console.log('a is: ' + a)
    if(a && b && !c) console.log('a+b is: ' + (a+b))
    if(a && b && c) console.log('a+b+c is: ' + (a+b+c))
};

print(1)
print(1,2)
print(1,2,3)

4.8.5 可调用和可实例化

  1. 可调用
// 可调用
interface ReturnString {
    (): string
}
// 它表示一个返回值为string的函数
declare const foo: ReturnString
const bar = foo()

一个实例

interface Complex {
    (foo: string, bar?: number, ...others: boolean[]): number
}

interface Overloaded {
    (foo: string): string;
    (foo: number): number;
}

function stringOrNumber(foo: number): number;
function stringOrNumber(foo: string): string;
function stringOrNumber(foo: any): any {
    if(typeof foo === 'number'){
        return foo * foo;
    } else if (typeof foo === 'string'){
        return `hello ${foo}`;
    }
}

const overloaded: Overloaded  = stringOrNumber

const str = overloaded('')   // str被推断为string类型
const num = overloaded(123) // num 被推断为number类型
console.log(str)
console.log(num)
  1. 可实例化
   function create<T>(c: {new(): T; }): T {
       return new c();
   }
   
   class BeeKeeper {
       hasMask: boolean;
   }
   
   class ZooKeeper {
       nametag: string;
   }
   
   class Animal {
       numLegs: number;
   }
   
   class Bee extends Animal {
       keeper: BeeKeeper;
   }
   
   class Lion extends Animal {
       keeper: ZooKeeper;
   }
   
   function createInstance<A extends Animal>(c: new () => A): A {
       return new c();
   }
   
   createInstance(Lion).keeper.nametag;  // typechecks!
   createInstance(Bee).keeper.hasMask;   // typechecks!

4.9 类型断言

TypeScript允许你以任何方式去重写其推断和分析的类型。这是通过一种被称为“类型断言”的机制来实现的。TypeScript类型断言纯粹是用来告诉编译器,你比它更了解这个类型,并且它不应该再发出错误提示

类型断言的一个常见示例是将代码从JavaScript迁移到TypeScript。

const foo = {}
foo.bar = 123

这里的代码发出了错误警告,因为foo的类型推断为{},即具有零属性的对象。因此,你不能在它的属性上添加bar或bas,你可以通过类型断言来避免此问题。

const foo = {} as any
foo.bar = 123
console.log(foo)

也可以这样进行类型断言

const foo = <any>{}
foo.bar = 123
console.log(foo)

但是因为JSX中JS和XML混写,因此推荐使用as来进行类型断言

类型断言之所以不被称为“类型转换”,是因为转换通常意味着某种运行时的支持。但是,类型断言纯粹是一个编译时语法,同时,它也为编译器提供了分析代码的方法。

我们可以也可以利用类型断言来提供代码提示

interface People {
    name: string;
    age: number;
    hobby: string;
}
const person = <People>{
    name: 'david',
    age: 23,
    hobby: 'programming'
}

console.log(person)

这也会存在一个同样的问题,如果你忘了某个属性,编译器同样也不会发出错误警告。那么,像这样可能会更好一些。

const person1: People = {
    name: 'tom',
    age: 23
}

尽管我们已经证明了类型断言并不那么安全,但它也有用武之地。下面就是一个非常实用的例子,当使用者了解传入参数更具体的类型时,类型断言就能按预期工作。

const handler = (event: Event) => {
    const mouseEvent = event as MouseEvent
}

4.10 Freshness

对于对象字面量的类型,TypeScript 有一个被称之为 「Freshness 」的概念,它也被称为更严格的对象字面量检查

function logName(something: {name?: string}){
    console.log(something.name)
}
const p1 = {name: 'david', job: 'programming'}
// 结构类型很方便,但是, 如果它允许传入并没有用到的属性
logName(p1)
// 对于对象字面量会进行Fressness检查(更为严格的对象字面量检查)
logName({name: 'jack', job: 'backendprogrammer'})
// 可以通过添加索引签名来允许字面量对象传递没有显示声明的属性
function logName1 (something: {name?: string, [x:string]: any}) {
    console.log(something.name)
}
logName1({name: 'jack', job: 'backendprogrammer'})

4.11 类型保护

4.11.1 使用typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量或对象的类型。

interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // type Sem = Person

typeof 操作符除了可以获取对象的结构类型之外,它也可以用来获取函数对象的类型

function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

4.11.2 使用instanceof

class Foo {
    foo = 123;
    common = '123';
}

class Bar {
    bar = 123;
    common = '123'
}

function doStuff(arg: Foo | Bar){
    if (arg instanceof Foo){
        console.log(arg.foo)
        console.log(arg.bar)
        console.log(arg.common)
    }
    if (arg instanceof Bar) {
        console.log(arg.foo)
        console.log(arg.bar)
        console.log(arg.common)
    }
}

typescript能够推断出else中的类型

class Foo {
    foo = 123;
    common = '123';
}

class Bar {
    bar = 123;
    common = '123'
}

function doStuff(arg: Foo | Bar){
    if (arg instanceof Foo){
        console.log(arg.foo)
        console.log(arg.bar)
        console.log(arg.common)
    } else {
        console.log(arg.bar)
    }
}

4.11.3 字面量类型保护

你可以使用===、、!、!=来区分字面量类型。

type Foo = {
    kind: 'foo';
    foo: number;
}
type Bar = {
    kind: 'bar';
    bar: number;
}
function doStuff(arg: Foo|Bar) {
    if(arg.kind === 'foo'){
        console.log(arg.foo)
    } else {
        console.log(arg.bar)
    }
}

4.11.4 strictNullChecks编译属性下的null和undefined

TypeScript可以使用==null和!==null来区分null和undefined

4.11.5 自定义类型保护

interface Foo {
    foo: number;
    common: string;
}
interface Bar {
    bar: number;
    common: string;
}
function isFoo(arg: Foo|Bar): arg is Foo {
    return (arg as Foo).foo !== undefined
}
function doStuff(arg: Foo | Bar) {
    if(isFoo(arg)){
        console.log(arg.foo)  // 正确
        console.log(arg.bar)  // 错误
    } else {
        console.log(arg.bar)  // 正确
        console.log(arg.foo)  // 错误
    }
}

4.11.6 类型保护和函数回调

TypeScript不能确定类型保护在回调中一直有效,比如下述代码中变量foo的属性bar可能为为undefined,因此typescript会报错

export var foo:{bar?: {baz: string}}
function doCalback(callback: () => void){
    callback()
}
if(foo.bar){
    console.log(foo.bar.baz)
    doCalback(() => {
        console.log(foo.bar.baz)
    })
}

解决方式为把推断的安全值存放在本地的局部变量中

export var foo:{bar?: {baz: string}}
function doCalback(callback: () => void){
    callback()
}
if(foo.bar){
    console.log(foo.bar.baz)
    const bar = foo.bar
    doCalback(() => {
        console.log(bar.baz)
    })
}

4.12 字面量类型

4.12.1 字符串字面量

type Direction = 'North' | 'East' | 'South' | 'West'
function move(distance: number, direction: Direction){
    console.log(`${direction}: ${distance}`)
}
move(1, 'North')
move(2, "West")

4.12.3 boolean和number字面量

type oneToFive = 1 | 2 | 3 | 4 | 5
type Bools = true | false
function trueOrFalse(arg: Bools) {
    console.log(arg)
}
function logNumber(num: oneToFive){
    console.log(num)
}
trueOrFalse(false)
logNumber(1)

4.12.4 基于字符串的枚举

// 根据传入的数组生成key-value结构
function strEnum<T extends string>(o: Array<T>): {[K in T]: K} {
    return o.reduce((res, key) => {
        res[key] = key;
        return res;
    }, Object.create(null))
}
// 然后可以使用keyof, typeof来生成字符串的联合类型
const Direction = strEnum(['North', 'South', 'East', 'West'])
type Direction = keyof typeof Direction
let sample: Direction

sample = Direction.North
sample = 'North'
sample = 'AnythingElse' // 错误

4.13 readonly

使用readonly关键字来标记属性可以保证数据不可变

我们也可以指定一个类的属性为readonly,然后在声明或在构造函数中初始化它们

4.13.1 使用readonly的实例

  1. 一个把类型中所有属性转化为只读属性的映射类型
// 下面是一个readonly映射类型,接收一个泛型T,用来把它所有属性都标记为readonly
type Foo =  {
    bar: number;
    bas: number;
}

type MyReadonly<T> = {
    readonly [K in keyof T]: T[K]
}
type ReadonlyFoo = MyReadonly<Foo>;

const foo1: Foo = {bar: 123, bas: 456}
const fooReadonly: ReadonlyFoo = {bar: 123, bas: 456}
foo.bar = 456
fooReadonly.bar = 789   // 报错 
  1. React.js中state和props的使用
import React, { PureComponent } from 'react'
import { Layout, Menu } from 'antd'
import { connect } from 'dva'
import { Link } from 'react-router-dom'
import { Urls } from '@/config'

const { Header, Content } = Layout

interface Props {
    readonly [index: string]: any
}
interface State {
    menus: Array<{
        readonly key: string
        readonly label: string
        readonly to: string
    }>
}
class App extends PureComponent<Props, State> {
    state = {
        menus: [
            {
                key: 'main',
                label: '首页',
                to: Urls.MAIN
            },
            {
                key: 'form',
                label: '表单设计',
                to: Urls.FORM
            },
            {
                key: 'code',
                label: '代码生成',
                to: Urls.CODE
            },
            {
                key: 'chart',
                label: '图表设计',
                to: Urls.CHART
            }
        ]
    }

    renderMenu(): JSX.Element {
        const { menus } = this.state
        return (
            <Menu theme={'dark'} mode={'horizontal'}>
                {menus.map((menu) => (
                    <Menu.Item key={menu.key}>
                        <Link to={menu.to}>{menu.label}</Link>
                    </Menu.Item>
                ))}
            </Menu>
        )
    }

    render(): JSX.Element {
        return (
            <Layout className={'height100'}>
                <Header>{this.renderMenu()}</Header>
                <Content>{this.props.children}</Content>
            </Layout>
        )
    }
}

export default connect(({ app }) => ({ ...app.toJS() }))(App)

  1. 绝对不可变
   interface Foo {
       readonly [x: number]: number;
   }
   const foo1: Foo = { 0: 123, 2: 345 }
   console.log(foo1[0])
   foo1[0] = 456   // 报错,只读不可修改
   // 以不变的方式使用原生数组,可以使用TypeScript提供的ReadonlyArray接口
   let foo2: ReadonlyArray<number> = [1,2,3]
   console.log(foo2[0])
   foo2.push(4)  // 报错,只读数组不可添加元素
   foo2 = foo2.concat(4)  // 这样是允许的,因为通过concat创建了一个副本
  1. readonly的自动推断
    例如在一个class中,如果有一个只有getter、没有setter的属性,它就能被推断为是只读的
   class Animals {
       name: string = 'dog';
       age: number = 2;
   
       get info() {
           return this.name + '' + this.age;
       }
   }
   
   const animal1 = new Animals()
   console.log(animal1.info)
   animal1.info = 'cat2'   // 报错,只读属性不可重新赋值
  1. readonly和const
    readonly用于属性, const用于变量声明
    readonly能确保属性不能直接被使用者修改,但是当你把这个属性交给其他并没有做出这种保证的使用者(出于类型兼容的原因而被允许)时,他们可以修改它

    const foo1: {
        readonly bar: number;
    } = {
        bar: 123
    }
    
    foo1.bar = 456   // 报错,只读属性不能被修改
    function iMutateFoo(foo: { bar: number }) {
        // 处于类型兼容考虑readonly的属性被修改了
        foo.bar = 456
    }
    
    iMutateFoo(foo1)
    
    interface Foo {
        readonly bar: number
    }
    function iMutateFoo1(foo: Foo) {
        foo.bar = 456  // 因为明确了参数的类型,则此处只读属性bar不可被修改了
    }
    iMutateFoo1(foo1)
    
    

4.14 泛型

设计泛型的关键动机是在成员之间提供有意义的类型约束,这些成员可以是类的实例成员、类的方法、函数的参数、函数返回值。

4.14.1 使用泛型的实例

  1. 一个队列的例子
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(1)
queue.push('1')  // 报错,指定泛型类型为number类型,不能再添加其他类型的元素了
  1. reverse函数
   function reverse <T> (arr: T[]): T[] {
       const res: T[] = []
       for( let i = arr.length - 1; i >= 0; i-- ){
           res.push(arr[i])
       }
       return res
   }
   
   console.log(reverse([1, 2, 3]))

建议:你可以随意调用泛型的参数,当你使用简单的泛型时,泛型常用T、U、V表示。如果在你的参数里,拥有不止一个泛型,你应该使用一个更语义化的名称,如TKey和TValue。依照惯例,以T作为泛型的前缀
3. 一个用于加载JSON文件的返回值函数, 它会返回任何你传入的类型的Promise

   const getJSON = <T>(config: {url: string; headers?: {[key: string]: string}}): Promise<T> => {
       const fetchConfig = {
           method: 'GET',
           Accept: 'application/json',
           'Content-Type': 'application/json',
           ...(config.headers || {})
       }
       return fetch(config.url, fetchConfig).then<T>(response => response.json());
   }
   
   type LoadUserResponse = {
       user: {
           name: string;
           emial: string;
       }
   };
   
   function loadUsers(){
       return getJSON<LoadUserResponse>({ url: 'http://example.com/users'});
   }
   
   loadUsers().then(res => {
       console.log(res.user.name)
   })
  1. 泛型被用于函数参数
   type Animals =  {
       name: string;
       age: number;
   }
   
   type Website = {
       url: string;
       count: number;
   }
   
   function log<T>(arg: T){
       console.log(arg)
   }
   
   log<Animals>({
       name: 'dog',
       age: 2
   })
   log<Website>({
       url: 'http://baidu.com',
       count: 10000
   })

4.15 类型推断

TypeScript可以根据一些简单的规则来推断(然后检查)变量的类型

  • 定义变量
    变量的类型根据定义来推断
 const a = 123  // 推断为number
 let b = 'hello' // 推断为string
 b = 123      // 错误
  • 函数返回值
    根据return语句来判断
 function add(a: number, b: number){
     return a + b;
 }
 // 推断返回类型为number
  • 赋值
  type Adder = (a: number, b:number) => number
  let foo1: Adder = (a, b) => a + b   // a,b被推断为number类型
  • 对象字面量
  const obj = {
      a: 1,
      b: '1'
  }
  obj.a = '2'  // 报错
  • 解构
  const obj = {
      a: 1,
      b: '1'
  }
  let { a } = obj
  a = '2'  // 报错

4.15.1 noImplicitAny

标记noImplicitAny用来指示编译器,在无法推断一个变量的类型时,发出一个错误(或者只将其作为一个隐式的any)。此时,你可以做如下处理。

  • 通过显式添加:any的类型注解,来让它成为一个any类型。
  • 通过一些更准确的类型注解来帮助TypeScript推断类型。

4.16 类型的兼容性

类型兼容性用于确定一个类型能否赋值给其他类型。如string类型与number类型不兼容,因此不能相互赋值。

4.16.1 稳定性

TypeScript类型系统设计得很方便,它允许你有一些不正确的行为。例如,任何类型都能被赋值给any,这意味着编译器允许你做任何想做的事情。

let foo1: any = 123
foo1 = 'hello'

4.16.2 结构化类型的兼容性

TypeScript对象是一种结构化的类型,这意味着只要结构匹配,名称也就无关紧要了

interface Point {
    x: number;
    y: number;
}

class Point2D {
    constructor(public x: number, public y: number){}
}

let p: Point;
p = new Point2D(1, 2)

4.16.3 函数的兼容性

当你比较两个函数类型是否兼容时,下面是一些需要考虑的事情

  1. 返回类型
    类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
  2. 参数数量
    要查看 x 是否能赋值给 y,首先看它们的参数列表,x 的每个参数必须能在 y 里找到对应类型的参数,注意的是参数的名字相同与否无所谓,只看它们的类型。
   let x = (a: number) => 0;
   let y = (b: number, s: string) => 0;
   
   y = x; // OK
   x = y; // Error
  1. 可选参数和rest参数
    为了方便起见,可选参数(预先确定的)和rest参数(任何数量的参数)是兼容的
   let aoo = (x: number, y: number) => {};
   let bar = (x?: number, y?:number) => {};
   let bas = (...args: number[]) => {};
   
   aoo = bar
   bas = bar
   bas = aoo
   // 可选的参数(如上面例子中的bar)与不可选的参数(如上面例子中的foo),只有在strictNullChecks为false时兼容
  1. 函数参数类型兼容性
  export interface Event {
      timestamp: number;
  }
  export interface MouseEvent extends Event {
      x: number;
      y: number;
  }
  
  export interface KeyEvent extends Event{
      keyCode: number;
  }
  
  
  enum EventType {
      Mouse,
      KeyBoard
  }
  
  function addEventListener(eventType: EventType, handler: (n: Event) => void) {}
  
  // 不安全,但是常用
  const handler = ((e: MouseEvent) => console.log(e.x+ ',' + e.y))
  addEventListener(EventType.Mouse, handler) // 报错
  // 解决方案
  const handler1 = <(e: Event) => void>((e: MouseEvent) => console.log(e.x+ ',' + e.y))
  addEventListener(EventType.Mouse, handler1) // 报错
  // 或者
  const handler2 = ((e: Event) => console.log((<MouseEvent>e).x+ ',' + (<MouseEvent>e).y))

4.16.4 枚举类型的兼容性

  1. 枚举与数字类型是兼容的
   enum Status {
       Ready,
       Waiting
   }
   let status1 = Status.Ready
   let num1 = 0
   status1 = num
  1. 来自不同枚举的枚举变量,被认为是不兼容的
   enum Status {
       Ready,
       Waiting
   }
   enum Color {
       Red,
       Blue,
       Green
   }
   let status1 = Status.Ready
   let color1 = Color.Blue
   status1 = color1  // 报错

4.16.5 类的类型兼容

只比较实例成员和方法,构造函数和静态成员不起作用

class Animal1 {
    static feet: number;
    constructor(name: string, numFeet: number){}
}

class Size {
    static feet: number;
    constructor(meters: number){}
}

let a: Animal1
let s: Size = new Size(1)
a = s  // 可以赋值
s = a  // 可以赋值

私有的和受保护的成员必须来自相同的类

class Animal1 {
    protected feet: number | undefined;
}

class Cat extends Animal1{}

let animal1: Animal1;
let cat1: Cat = new Cat();
animal1 = cat1  // 可以赋值
cat1 = animal1 // 可以赋值

class  Size {
    protected feet: number | undefined;
}

let size: Size;
animal1 = size // 错误, 不能赋值
size = animal1  // 错误,不能赋值

4.16.6 泛型类型兼容

TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性

interface Empty<T> {}

let x: Empty<number>;
let y: Empty<string>;

// @ts-ignore
x = y
// 泛型T因为没有被**任何接口的任何成员**使用,因此并不会影响到兼容性

如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:

// @ts-ignore
let identity = function<T> (x: T): T {
    // ...
}

// @ts-ignore
let reverse = function<U> (y: U): U {
    //...
}

identity = reverse // (x: any) => any 类型与(y: any) => any匹配

当泛型被成员使用时,它将在实例化泛型后影响兼容性

interface Empty<T> {
    data: T;
}
let x: Empty<number>;
let y: Empty<string>;

x = y // 错误,不能赋值, 因为泛型T被interface Empty的成员data使用了

4.17 never

never类型是任何类型的子类型,也可以赋值给任何类型, 然而,没有类型是never的子类型或可以赋值给never类型(除了never类型本身之外),即使any也不可以赋值给never类型,通常表现为抛出异常或者无法执行到终止点(例如无限循环)。

let x: never;
let y: number;

// 运行错误,数字类型不能转为 never 类型
x = 123;

// 运行正确,never 类型可以赋值给 never类型
x = (()=>{ throw new Error('exception')})();

// 运行正确,never 类型可以赋值给 数字类型
y = (()=>{ throw new Error('exception')})();

// 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
    throw new Error(message);
}

// 返回值为 never 的函数可以是无限循环这种无法被执行到的终止点的情况
function loop(): never {
    while (true) {}
}

4.18 辨析联合类型

当类中含有字面量成员时,我们可以用该类的属性来辨析联合类型
如果你使用类型保护风格的检查(即==、=、!=、!),或者使用具有判断性的属性(在这里是kind),TypeScript将会认为你所使用的对象类型必须要具有特定的字面量,并且为你进行类型缩小。

interface Square {
    kind: 'square';
    size: number;
}
interface Rectangle {
    kind: 'rectangle';
    width: number;
    height: number;
}

interface Circle {
    kind: 'circle';
    radius: number;
}

type Shape = Square | Rectangle | Circle

function area(s: Shape){
    if(s.kind === 'square') {
        return s.size  * s.size
    } else if(s.kind === 'rectangle') {
        return s.width * s.height
    } else if(s.kind === 'circle') {
        return Math.PI * s.radius ** 2;
    } else {
        const _exhaustiveCheck: never = s
        return _exhaustiveCheck
    }
}

function areaBySwitch(s: Shape){
    switch (s.kind) {
        case 'square':
            return s.size * s.size;
        case 'rectangle':
            return s.width * s.height;
        case 'circle':
            return Math.PI * s.radius ** 2;
        default:
            const _exhaustiveCheck: never = s
            return _exhaustiveCheck
    }
}

4.18.1 版本控制

type DTO = | {
    version: undefined;
    name: string;
} | {
    version: 1;
    firstName: string;
    lastName: string;
} | {
    version: 2;
    firstName: string;
    lastName: string;
    middleName: string;
}

function printDTO(dto: DTO){
    if(dto.version == null){
        console.log('====================================');
        console.log(dto.name);
        console.log('====================================');
    } else if (dto.version === 1){
        console.log(dto.firstName, dto.lastName);
    } else if(dto.version === 2){
        console.log('====================================');
        console.log(dto.firstName, dto.middleName, dto.lastName);
        console.log('====================================');
    } else {
        const _exhaustiveCheck: never = dto;
    }
}

4.18.2 redux中定义ActionTypes的应用

import { createStore  } from 'redux'
type ActionTypes = 'INCREMENT' | 'DECREMENT'
type Action = {
    type: ActionTypes
}
const counter = (state = 0, action: Action) => {
    switch(action.type){
        case 'INCREMENT':
            return state + 1;
        case 'DECREMENT':
            return state - 1;
        default:
            return state;
    }
}
let store = createStore(counter);
store.subscribe(() => {
    console.log(store.getState())
})
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })

4.19 索引签名

4.19.1 JavaScript中时候用对象作为key进行签名默认调用toString()

可以用字符串访问JavaScript中的对象(在TypeScript中也一样),并保存对其他对象的引用除字符串外,它也可以保存任意的JavaScript对象,例如一个类的实例

const obj = {
    toString(){
        console.log('to string is called')
    }
}

let foo = {}
foo[obj] = 'hello'

当你向索引签名传入一个其他对象时,JavaScript会在得到结果之前先调用toString方法。
只要索引位置使用了obj,toString方法都将被调用。

const obj = {
    toString(){
        console.log('to string is called')
    }
}

let foo = {}
foo[obj] = 'hello'
console.log(foo[obj])

4.19.2 TypeScript索引签名

首先,因为JavaScript在任何一个对象索引签名上都会隐式调用toString方法,而在TypeScript中它将会抛出一个错误提示
我们必须这么做才能解决报错

const obj = {
    toString(){
        return 'Hello'
    }
}
const foo1: any = {}
foo[obj.toString()] = 'World'

1. 声明索引签名

const mailBox: {
  [index: string]: {
    content: string;
  };
} = {};
mailBox['a'] = {
    content: 'a'
}
mailBox['b'] = {
    content: 'b'
}

索引签名的名称,如{[index:string]:{message:string}}里的index,除提高了可读性外,并没有任何意义。例如,如果有一个用户名,你可以使用{username:string}:{message:string},这有助于下一个开发者理解你的代码。
所有成员必须符合字符串索引签名

2. 使用一组有限的字符串字面量

type keys = 'name' | 'age' | 'job'
type Index = { [k in keys]?: number | string}
const obj: Index = {
    name: 'david',
    age: 26,
    job: 'frontender'
}

3. 同时拥有string和number类型的索引签名

interface Obj {
    [key: string]: string | number;
    [index: number]: string;
}
let obj: Obj = {}
obj['a'] = 3
obj[1] = '1'

4. 索引签名的嵌套

interface NestedCSS {
    color?: string;  // 在strictNullChecks = false时,索引签名可以为undefined
    [selector: string]: string | NestedCSS;
}

const example: NestedCSS = {
    color: 'red',
    '.subclass': {
        color: 'blue'
    }
}

尽量不要用这种方式把字符串索引签名与有效变量混合使用。否则,如果属性名称中有拼写错误,这个错误将不会被捕获。

相反,我们要把索引签名分离到自己的属性里,如命名为nest,或children、subnodes等。

interface NestedCSS {
    color?: string;  // 在strictNullChecks = false时,索引签名可以为undefined
    nest?: {
        [selector: string]: NestedCSS
    }
}

const example: NestedCSS = {
    color: 'red',
    nest: {
        '.subclass': {
            color: 'blue'
        },
        '.subclass1': {
            color: 'red'
        }
    }
}

这个时候如果属性名出现拼写错误就能正常进行错误提示

4.20 错误处理

function validate(
  value: number
): {
  error?: string;
} {
  if (value < 0 || value > 100) {
    return { error: "Invalid Value" };
  }
}

4.21 混合

// 定义构造函数类型
type Constructor<T = {}> = new (...args: any[]) => T;

// 添加属性的混合实例
function TimesTamped<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        timestamp = Date.now();
    }
}

// 添加属性和方法的混合实例
function Activatable<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        isActivated = false;
        activated(){
            this.isActivated = true;
        }
        deactivated(){
            this.isActivated = false;
        }
    }
}
// 简单类
class User {
    name = '';
    constructor(name?: string){
        this.name = name;
    }
}

// 通过混合获得有时间戳的用户
const TimestampUser = TimesTamped(User);
const timestampUser = new TimestampUser();
console.log(timestampUser.timestamp);

// 通过混合获取有时间戳并可以激活禁用的用户
const TimestampActivatedUser = TimesTamped(Activatable(User));
const timestampActivatedUser = new TimestampActivatedUser('david');
console.log(timestampActivatedUser.name, timestampActivatedUser.timestamp)

4.22 ThisType

通过ThisType,我们可以在对象字面量中输入this,并提供通过上下文类型控制this类型,它只有在–noImplicitThis的选项下才有效。

type ObjectDescriptor<D, M> = {
  data?: D;
  methods: M & ThisType<D & M>;
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods;
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
    },
  },
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
console.log(obj.x, obj.y)

你可能感兴趣的:(前端追梦人TypeScript教程)