深入了解TypeScript

221 阅读29分钟

!

TypeScript = Type + Script(标准JS)。我们从TS的官方网站上就能看到定义:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript。TypeScript是一个编译到纯JS的有类型定义的JS超集。

  • 用于JavaScript环境,是JavaScript的超集(包括es5-至今)
  • 提供了静态类型检查(只编译时检测 减少线上运行时错误)也支持动态类型
  • 提供类似基于类的面向对象编程语法 (public protect private 私有字段# 函数重载 abstract抽象类 封装 接口等)

原因

  • JavaScript中的数据类型可以分为基础数据类型和引用数据类型
  • 动态类型语言 弱类型语言 无意中 强类型转换
  • ESLint等工具检测不出来的。

其它优点

可以根据类型标注自动生成文档。详情请参阅 typedoc

Typescript

TypeScript 工作流程

TypeScript 只会在编译阶段对类型进行静态检查,如果发现有错误,编译时就会报错。而在运行时,编译生成的 JS 与普通的 JavaScript 文件一样,并不会进行类型检查。

TtypeScript 基础静态类型和对象类型

在 TypeScript 静态类型分为两种,

  • 基础静态类型
    • null
    • undefined
    • String
    • Boolean
    • Symbol
    • Number
  • 对象类型
    • 对象类型
    • 数组类型
    • 类类型
    • 函数类型

TypeScript 基础类型

  • Boolean
  • Number
  • String
  • Symbol
  • Array
  • Enum (正向映射 & 反向映射)
    • 数字枚举
    • 字符串枚举
    • 常量枚举
    • 异构枚举
  • Any (所有类型都可以赋值)
  • Unknown (所有类型都可以赋值)
  • Tuple (元组 =>? argument)
  • Void (多用于函数返回)
  • Null
  • Undefined
  • object & Object & {}
    • object
    • Object
    • {}
  • Never
let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];

let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number;
// type Foo = string | number | boolean;
function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

类型注释和类型推断

类型推断

let countInference = 123;

这时候我并没有显示的告诉你变量countInference是一个数字类型,但是如果你把鼠标放到变量上时,你会发现 TypeScript 自动把变量注释为了number(数字)类型,也就是说它是有某种推断能力的,通过你的代码 TS 会自动的去尝试分析变量的类型。这个就彷佛是人的情商比较高,还没等女生表白那,你就已经看出她的心思。

TypeScript 断言

类型断言

“尖括号” <> 语法

as 语法

回想一下怎么写类型断言:

var foo = <foo>bar;

这里断言bar变量是foo类型的。 因为TypeScript也使用尖括号来表示类型断言,在结合JSX的语法后将带来解析上的困难。因此,TypeScript在.tsx文件里禁用了使用尖括号的类型断言。

由于不能够在.tsx文件里使用上述语法,因此我们应该使用另一个类型断言操作符:as。 上面的例子可以很容易地使用as操作符改写:

var foo = bar as foo;

as操作符在.ts和.tsx里都可用,并且与尖括号类型断言行为是等价的。

非空断言

具体而言,x! 将从 x 值域中排除 null 和 undefined 。

忽略 undefined 和 null 类型

function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // Ok
}

调用函数时忽略 undefined 类型

type NumGenerator = () => number;

function myFunc(numGenerator: NumGenerator | undefined) {
  // Object is possibly 'undefined'.(2532)
  // Cannot invoke an object which is possibly 'undefined'.(2722)
  const num1 = numGenerator(); // Error
  const num2 = numGenerator!(); //OK
}

确定赋值断言

let x: number;
initialize();
// Variable 'x' is used before being assigned.(2454)
console.log(2 * x); // Error

function initialize() {
  x = 10;
}
let x!: number;
initialize();
console.log(2 * x); // Ok

function initialize() {
  x = 10;
}

通过 let x!: number; 确定赋值断言,TypeScript 编译器就会知道该属性会被明确地赋值。

类型守卫 (运行时检查)

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。

in 关键字

typeof 关键字

instanceof 关键字

自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

联合类型和类型保护

联合类型 '|'

类型保护 'as'

交叉类型

在 TypeScript 中交叉类型是将多个类型合并为一个类型。通过 & 运算符可以将现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

let point: Point = {
  x: 1,
  y: 1
}

同名基础类型属性的合并

混入后的类型可能为 never。

同名非基础类型属性的合并

混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合并。

TypeScript 函数

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  // type Combinable = string | number;
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}
function getNumber({ one }: { one: number }): number {
  return one;
}

const one = getNumber({ one: 1 });

TypeScript 类的构造函数

类继承中的构造器写法

普通类的构造器我们已经会了,在子类中使用构造函数需要用super()调用父类的构造函数。这时候你可能不太理解我说的话,我们还是通过代码来说明(详细说明在视频中讲述)。

class Person{
    constructor(public name:string){}
}

class Teacher extends Person{
    constructor(public age:number){
        super('jspang')
    }
}

const teacher = new Teacher(18)
console.log(teacher.age)
console.log(teacher.name)

这就是子类继承父类并有构造函数的原则,就是在子类里写构造函数时,必须用super()调用父类的构造函数,如果需要传值,也必须进行传值操作。就是是父类没有构造函数,子类也要使用super()进行调用,否则就会报错。

class Person{}

class Teacher extends Person{
    constructor(public age:number){
        super()
    }
}

const teacher = new Teacher(18)
console.log(teacher.age)

TypeScript 类的 Getter、Setter 和 static 使用

类的 Getter

类的 Setter

类中的 static

学习类,都知道要想使用这个类的实例,就要先New出来(),但有时候人们就是喜欢走捷径,在们有对象的情况下,也想享受青春的躁动,有没有方法?肯定是有方法的。 比如我们先写一下最常规的写法:

class Girl {
  sayLove() {
    return "I Love you";
  }
}

const girl = new Girl();
console.log(girl.sayLove());

但是现在你不想new出对象,而直接使用这个方法,那TypeScript为你提供了快捷的方式,用static声明的属性和方法,不需要进行声明对象,就可以直接使用,代码如下。

class Girl {
  static sayLove() {
    return "I Love you";
  }
}
console.log(Girl.sayLove());

静态修饰符static,这样就不用 new 出对象就可以使用类里的方法了。

类的只读属性和抽象类

readonly

abstract

什么是抽象类那?我给大家举个例子,比如我开了一个红浪漫洗浴中心,里边有服务员,有初级技师,高级技师,每一个岗位我都写成一个类,那代码就是这样的。(注释掉刚才写的代码)

class Waiter {}

class BaseTeacher {}

class seniorTeacher {}

我作为老板,我要求无论是什么职位,都要有独特的技能,比如服务员就是给顾客倒水,初级技师要求会泰式按摩,高级技师要求会 SPA 全身按摩。这是一个硬性要求,但是每个职位的技能有不同,这时候就可以用抽象类来解决问题。 抽象类的关键词是abstract,里边的抽象方法也是abstract开头的,现在我们就写一个Girl的抽象类。

abstract class Girl{
    abstract skill()  //因为没有具体的方法,所以我们这里不写括号
}

有了这个抽象类,三个类就可以继承这个类,然后会要求必须实现skill()方法,代码如下:

abstract class Girl{
    abstract skill()  //因为没有具体的方法,所以我们这里不写括号

}

class Waiter extends Girl{
    skill(){
        console.log('大爷,请喝水!')
    }
}

class BaseTeacher extends Girl{
    skill(){
        console.log('大爷,来个泰式按摩吧!')
    }
}

class seniorTeacher extends Girl{
    skill(){
        console.log('大爷,来个SPA全身按摩吧!')
    }
}

TypeScript 数组

一般数组类型的定义

如果数组中有多种类型

const arr: (number | string)[] = [1, "string", 2];

数组中对象类型的定义

TypeScript 对象

TypeScript 接口 -> Interface

  • readonly
  • ?
  • ReadonlyArray<T>
  • Array<T>
  • interface
  • type
  • Extend
  • &
  • Implements
  • Declaration merging (声明合并)

ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

任意属性

interface Person {
  name: string;
  age?: number;
  [propName: string]: any;
}
const p1 = { name: "semlinker" };
const p2 = { name: "lolo", age: 5 };
const p3 = { name: "kakuqo", sex: 1 }

接口与类型别名的区别

TypeScript 类

"use strict";
var Greeter = /** @class */ (function () {
    // 构造函数 - 执行初始化操作
    function Greeter(message) {
      this.greeting = message;
    }
    // 静态方法
    Greeter.getClassName = function () {
      return "Class name is Greeter";
    };
    // 成员方法
    Greeter.prototype.greet = function () {
      return "Hello, " + this.greeting;
    };
    // 静态属性
    Greeter.cname = "Greeter";
    return Greeter;
}());
var greeter = new Greeter("world");

私有字段 or 私有名称

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

访问器 getter & setter

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

抽象类

使用 abstract 关键字声明的类,我们称之为抽象类。抽象类不能被实例化,因为它里面包含一个或多个抽象方法。所谓的抽象方法,是指不包含具体实现的方法:

abstract class Person {
  constructor(public name: string){}

  abstract say(words: string) :void;
}

// Cannot create an instance of an abstract class.(2511)
const lolo = new Person(); // Error

抽象类不能被直接实例化,我们只能实例化实现了所有抽象方法的子类。具体如下所示:

abstract class Person {
  constructor(public name: string){}

  // 抽象方法
  abstract say(words: string) :void;
}

class Developer extends Person {
  constructor(name: string) {
    super(name);
  }
  
  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}

const lolo = new Developer("lolo");
lolo.say("I love ts!"); // lolo says I love ts!

类方法重载

TypeScript 泛型 占位符

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

泛型语法

参考上面的图片,当我们调用 identity(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。

其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如:

  • Partial
  • Required
  • Readonly
  • Record
  • ReturnType

typeof

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

const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person

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

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

keyof

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

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number
interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number。

in

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

infer 推断

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

extends

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
loggingIdentity(3);  // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3});

Partial (局部的;偏爱的;不公平的)

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?。

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

interface Todo {
  title: string;
  description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}
const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};
const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});

在上面的 updateTodo 方法中,我们利用 Partial 工具类型,定义 fieldsToUpdate 的类型为 Partial,即:

{
   title?: string | undefined;
   description?: string | undefined;
}

TypeScript 装饰器

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

TypeScript 4.0 新特性

构造函数的类属性推断

当 noImplicitAny 配置属性被启用之后,TypeScript 4.0 就可以使用控制流分析来确认类中的属性类型:

标记的元组元素

// 已使用标签的智能提示
// addPerson(name: string, age: number): void
function addPerson(...args: [name: string, age: number]): void {
  console.log(`Person info: name: ${args[0]}, age: ${args[1]}`);
} 

编译上下文

tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。

tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

附加

事件类型

Event 事件对象类型

ClipboardEvent<T = Element> 剪贴板事件对象
DragEvent<T = Element> 拖拽事件对象
ChangeEvent<T = Element>  Change 事件对象
KeyboardEvent<T = Element> 键盘事件对象
MouseEvent<T = Element> 鼠标事件对象
TouchEvent<T = Element>  触摸事件对象
WheelEvent<T = Element> 滚轮事件对象
AnimationEvent<T = Element> 动画事件对象
TransitionEvent<T = Element> 过渡事件对象

事件处理函数类型

type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];
type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;

Promise 类型

在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。

Promise 是一个泛型类型,T 泛型变量用于确定使用 then 方法时接收的第一个回调函数(onfulfilled)的参数类型。

interface IResponse<T> {
  message: string,
  result: T,
  success: boolean,
}
async function getResponse (): Promise<IResponse<number[]>> {
  return {
    message: '获取成功',
    result: [1, 2, 3],
    success: true,
  }
}
getResponse()
  .then(response => {
    console.log(response.result)
  })
我们首先声明 IResponse 的泛型接口用于定义 response 的类型,通过 T 泛型变量来确定 result 的类型。
然后声明了一个 异步函数 getResponse 并且将函数返回值的类型定义为 Promise<IResponse<number[]>> 。
最后调用 getResponse 方法会返回一个 promise 类型,通过 then 调用,此时 then 方法接收的第一个回调函数的参数 response 的类型为,{ message: string, result: number[], success: boolean} 。
Promise<T> 实现源码  node_modules/typescript/lib/lib.es5.d.ts
interface Promise<T> {
    /**
     * Attaches callbacks for the resolution and/or rejection of the Promise.
     * @param onfulfilled The callback to execute when the Promise is resolved.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of which ever callback is executed.
     */
    then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
    /**
     * Attaches a callback for only the rejection of the Promise.
     * @param onrejected The callback to execute when the Promise is rejected.
     * @returns A Promise for the completion of the callback.
     */
    catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

typeof

一般我们都是先定义类型,再去赋值使用,但是使用 typeof 我们可以把使用顺序倒过来。

const options = {
  a: 1
}
type Options = typeof options

使用字符串字面量类型限制值为固定的字符串参数

限制 props.color 的值只可以是字符串 red、blue、yellow 。

interface IProps {
  color: 'red' | 'blue' | 'yellow',
}

使用数字字面量类型限制值为固定的数值参数

限制 props.index 的值只可以是数字 0、 1、 2 。

interface IProps {
 index: 0 | 1 | 2,
}

Partial

使用 Partial 将所有的 props 属性都变为可选值 Partial 实现源码 node_modules/typescript/lib/lib.es5.d.ts

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

上面代码的意思是 keyof T 拿到 T 所有属性名, 然后 in 进行遍历, 将值赋给 P , 最后 T[P] 取得相应属性的值,中间的 ? 用来进行设置为可选值。 如果 props 所有的属性值都是可选的我们可以借助 Partial 这样实现。

import { MouseEvent } from 'react'
import * as React from 'react'
interface IProps {
  color: 'red' | 'blue' | 'yellow',
  onClick (event: MouseEvent<HTMLDivElement>): void,
}
const Button: SFC<Partial<IProps>> = ({onClick, children, color}) => {
  return (
    <div onClick={onClick}>
      { children }
    </div>
  )

Required (-? 和 +?)

使用 Required 将所有 props 属性都设为必填项 Required 实现源码 node_modules/typescript/lib/lib.es5.d.ts 。

type Required<T> = { [P in keyof T]-?: T[P] };
看到这里,小伙伴们可能有些疑惑, -? 是做什么的,其实 -? 的功能就是把可选属性的 ? 去掉使该属性变成必选项,对应的还有 +? ,作用与 -? 相反,是把属性变为可选项。

条件类型

T extends U ? X : Y

原先

interface Id { id: number, /* other fields */ }
interface Name { name: string, /* other fields */ }
declare function createLabel(id: number): Id;
declare function createLabel(name: string): Name;
declare function createLabel(name: string | number): Id | Name;

复制代码使用条件类型

type IdOrName<T extends number | string> = T extends number ? Id : Name;
declare function createLabel<T extends number | string>(idOrName: T): T extends number ? Id : Name;

Exclude<T,U>

从 T 中排除那些可以赋值给 U 的类型。 从 T 中排除那些可以赋值给 U 的类型。

type Exclude<T, U> = T extends U ? never : T;

复制代码实例:

type T = Exclude<1|2|3|4|5, 3|4>  // T = 1|2|5 

复制代码此时 T 类型的值只可以为 1 、2 、 5 ,当使用其他值是 TS 会进行错误提示。

Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.

Extract<T,U>

从 T 中提取那些可以赋值给 U 的类型。 Extract实现源码 node_modules/typescript/lib/lib.es5.d.ts。

type Extract<T, U> = T extends U ? T : never;

复制代码实例:

type T = Extract<1|2|3|4|5, 3|4>  // T = 3|4

复制代码此时T类型的值只可以为 3 、4 ,当使用其他值时 TS 会进行错误提示:

Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.

Pick<T,K>

从 T 中取出一系列 K 的属性。 Pick 实现源码 node_modules/typescript/lib/lib.es5.d.ts。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

复制代码实例: 假如我们现在有一个类型其拥有 name 、 age 、 sex 属性,当我们想生成一个新的类型只支持 name 、age 时可以像下面这样:

interface Person {
  name: string,
  age: number,
  sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
  name: '小王',
  age: 21,
}

Record<K,T>

将 K 中所有的属性的值转化为 T 类型。 Record 实现源码 node_modules/typescript/lib/lib.es5.d.ts。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

复制代码实例: 将 name 、 age 属性全部设为 string 类型。

let person: Record<'name' | 'age', string> = {
  name: '小王',
  age: '12',
}

Omit<T,K>(没有内置)

从对象 T 中排除 key 是 K 的属性。 由于 TS 中没有内置,所以需要我们使用 Pick 和 Exclude 进行实现。

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>

复制代码实例: 排除 name 属性。

interface Person {
  name: string,
  age: number,
  sex: string,
}

let person: Omit<Person, 'name'> = {
  age: 1,
  sex: '男'
}

NonNullable

排除 T 为 null 、undefined。 NonNullable 实现源码 node_modules/typescript/lib/lib.es5.d.ts。

type NonNullable<T> = T extends null | undefined ? never : T;

复制代码实例:

type T = NonNullable<string | string[] | null | undefined>; // string | string[]

ReturnType

获取函数 T 返回值的类型。。 ReturnType 实现源码 node_modules/typescript/lib/lib.es5.d.ts。

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

复制代码infer R 相当于声明一个变量,接收传入函数的返回值类型。 实例:

type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<(s: string) => void>; // void

React in TypeScript

在 TypeScript 开发环境下写 React 组件,与 ES6 的区别主要就是 Props 和 State 的定义。如果是 ES6,大概是这样:

import PropTypes from 'prop-types';
class App extends React.PureComponent {
  state = {
    aState: '',
    bState: '',
  };
}

App.propTypes = {
  aProps: PropTypes.string.isRequired,
  bProps: PropTypes.string.isRequired,
};

或者

import PropTypes from 'prop-types';
class App extends React.PureComponent {
  static propTypes = {
    aProps: PropTypes.string.isRequired,
    bProps: PropTypes.string.isRequired,
  };
  state = {
    aState: '',
    bState: '',
  };
}

复制代码如果用 TypeScript 来写,大概是这样(本文的 interface 定义默认以 I 开头):

interface IProps {
  aProps: string;
  bProps: string;
}
interface IState {
  aState: string;
  bState: string;
}

class App extends React.PureComponent<IProps, IState> {
  state = {
    aState: '',
    bState: '',
  };
}

TS 环境中的 React 组件属性是静态的,也就是说可以做静态检查,并且在支持 TS 的 IDE 下写的过程中可以自动自动提示

ES6 环境中的 prop-types 属性是动态的,也就是运行期做检查,也不能做自动提示/补全。

Redux 是最流行的状态管理库,一般和 React 结合使用。在 TypeScript 和 ES6 环境下,Redux 代码写法区别不大,有个问题就是,React 和 Redux 的绑定函数 connect 需用函数写法,而不建议用装饰器写法。因为 TS 对 类装饰器的静态解析还不支持,用了装饰器写 connect 就不能利用 TS 的静态解析的好处了。

import * as React from 'react' 和 import React from 'react' 有什么区别

  • 第一种写法是将所有用 export 导出的成员赋值给 React ,导入后用 React.xxx 访问
  • 第二种写法仅是将默认导出(export default)的内容赋值给 React

interface VS type 的区别

interface -> 接口 type -> 类型别名

  • 这两个语法和用处好像一样,用起来基本一样,但是也有少许的不同。
  • 类型别名可以直接给类型,比如string,而接口必须代表对象。

相同点

都可以描述一个对象或者函数

interface User {
  name: string
  age: number
}
interface SetUser {
  (name: string, age: number): void;
}
type User = {
  name: string
  age: number
};
type SetUser = (name: string, age: number)=> void;

都允许拓展(extends)

interface 和 type 都可以拓展,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 extends interface 。 虽然效果差不多,但是两者语法不同。

interface extends interface

interface Name { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}

type extends type

type Name = { 
  name: string; 
}
type User = Name & { age: number  };

interface extends type

type Name = { 
  name: string; 
}
interface User extends Name { 
  age: number; 
}

type extends interface

interface Name { 
  name: string; 
}
type User = Name & { 
  age: number; 
}

不同点

type 可以而 interface 不行

type 可以声明基本类型别名,联合类型,元组等类型

// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat

// 具体定义数组每个位置的类型
type PetList = [Dog, Pet]

type 语句中还可以使用 typeof 获取实例的 类型进行赋值

// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement('div');
type B = typeof div

其他骚操作

type StringOrNumber = string | number;  
type Text = string | { text: string };  
type NameLookup = Dictionary<string, Person>;  
type Callback<T> = (data: T) => void;  
type Pair<T> = [T, T];  
type Coordinates = Pair<number>;  
type Tree<T> = T | { left: Tree<T>, right: Tree<T> };

interface 可以而 type 不行

interface 能够声明合并

interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string 
}
*/

任意加字段

interface Girl {
  name: string;
  age: number;
  bust: number;
  waistline?: number;
  [propname: string]: any;
}

TypeScript 中类的访问类型

public

public从英文字面的解释就是公共的或者说是公众的,在程序里的意思就是允许在类的内部和外部被调用.

protect

protected 允许在类内及继承的子类中使用

private

private 访问属性的意思是,只允许再类的内部被调用,外部不允许调用

私有字段?

TypeScript 中私有字段的真正问题在于它们在后台使用了 WeakMap。

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

私有字段与 private 的区别

通过观察上述代码,使用 # 号定义的 ECMAScript 私有字段,会通过 WeakMap 对象来存储,同时编译器会生成 __classPrivateFieldSet 和 __classPrivateFieldGet 这两个方法用于设置值和获取值。

配置文件 tsconfig.json

strict

ture or false

strictNullChecks

参数用于新的严格空检查模式,在严格空检查模式下,null 和 undefined 值都不属于任何一个类型,它们只能赋值给自己这种类型或者 any

{
    "compilerOptions": {

        /**************基础配置**************/
        /**************基础配置**************/
        /**************基础配置**************/

        /* 开启增量编译:TS 编译器在第一次编译的时候,会生成一个存储编译信息的文件,下一次编译的时候,会根据这个文件进行增量的编译,以此提高 TS 的编译速度 */
        // "incremental": true,
        /* 指定存储增量编译信息的文件位置 */
        // "tsBuildInfoFile": "./",

        /* 打印诊断信息 */
        // "diagnostics": true,
        /* 打印输出的文件 */
        // "listEmittedFiles": true,
        /* 打印编译的文件(包括引用的声明文件)*/
        // "listFiles": true,

        /* 指定 ECMAScript 的目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
        // "target": "es5",
        /* 指定模块代码的生成方式: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
        // "module": "commonjs",

        /* 指定要包含在编译中的库文件——引用类库——即申明文件,如果输出的模块方式是 es5,就会默认引入 "dom","es5","scripthost"  */
        /* 如果在 TS 中想要使用一些 ES6 以上版本的语法,就需要引入相关的类库 */
        // "lib": [],

        /* 允许编译 JS 文件 */
        // "allowJs": true,
        /* 检查 JS 文件*/
        // "checkJs": true,

        /* 指定 JSX 代码生成的模式: 'preserve', 'react-native', or 'react'. */
        /* 'react' 模式下:TS 会直接把 jsx 编译成 js */
        /* 'preserve' 模式下:TS 不会把 jsx 编译成 js,会保留 jsx */
        // "jsx": "preserve",


        /**************声明文件相关配置**************/
        /**************声明文件相关配置**************/
        /**************声明文件相关配置**************/

        /* 生成相应的类型声明文件 —— '.d.ts' */
        // "declaration": true,
        /* 声明文件的输出路径 */
        // "declarationDir": "./d",
        /* 只生成声明文件,不生成 JS */
        // "emitDeclarationOnly": true,
        /* 声明文件目录,默认 node_modules/@types */
        // "typeRoots": [],
        /* 要导入的声明文件包,默认导入上面声明文件目录下的所有声明文件 */
        // "types": [],


        /* 将多个相互依赖的文件合并并且把编译后的内容输出到一个文件里
         * 可以用在产出 AMD 模块的场景中
         * "module":"amd" 时,当一个模块引入了另外一个模块,编译的时候会把这两个模块的编译结果合并到一个文件中
         */
        // "outFile": "./",
        /* 指定编译文件的输出目录 */
        // "outDir": "./out",
        /* 指定输入文件的根目录,用于控制输出目录的结构 */
        // "rootDir": "./",

        /* 启用项目编译 */
        // "composite": true,

        /*  输出的时候移除注释 */
        // "removeComments": true,

        /* 不输出文件 */
        // "noEmit": true,
        /* 发生错误时不输出文件 */
        // "noEmitOnError": true,

        /* 不生成 helper 函数,以前的话设置为 true 后,需要额外安装 ts-helpers */
        /* 类似于 babel ,会给每个文件都生成 helper 函数,会使得最终编译后的包的体积变大 */
        // "noEmitHelpers": true,
        /* 现在可以通过 tslib(TS 内置的库)引入 helper 函数,!!!文件必须是模块 !!! */
        /* 编译后自动引入 var tslib_1 = require("tslib") */
        // "importHelpers": true,

        /* 当目标是 ES5 或 ES3 的时候提供对 for-of、扩展运算符和解构赋值中对于迭代器的完整支持 */
        // "downlevelIteration": true,

        /* 把每一个文件转译成一个单独的模块 */
        // "isolatedModules": true,


        /**************严格检查配置**************/
        /**************严格检查配置**************/
        /**************严格检查配置**************/

        /* 开启所有的严格检查配置 */
        "strict": true,
        /* 不允许使用隐式的 any 类型 */
        // "noImplicitAny": true,

        /* 不允许把 null、undefined 赋值给其他类型变量 */
        // "strictNullChecks": true,

        /* 不允许函数参数双向协变 */
        // "strictFunctionTypes": true,

        /* 使用 bind/call/apply 时,严格检查函数参数类型 */
        // "strictBindCallApply": true,

        /* 类的实例属性必须初始化 */
        // "strictPropertyInitialization": true,

        /* 不允许 this 有隐式的 any 类型,即 this 必须有明确的指向*/
        // "noImplicitThis": true,

        /* 在严格模式下解析并且向每个源文件中注入 "use strict" */
        // "alwaysStrict": true,

        /**************额外的语法检查配置,这种检查交给 eslint 就行,没必要配置**************/
        /**************额外的语法检查配置,这种检查交给 eslint 就行,没必要配置**************/
        /**************额外的语法检查配置,这种检查交给 eslint 就行,没必要配置**************/

        /* 有未使用到的本地变量时报错 */
        // "noUnusedLocals": true,

        /* 有未使用到的函数参数时报错 */
        // "noUnusedParameters": true,

        /* 每个分支都要有返回值 */
        // "noImplicitReturns": true,

        /* 严格校验 switch-case 语法 */
        // "noFallthroughCasesInSwitch": true,

        /**************模块解析配置**************/
        /**************模块解析配置**************/
        /**************模块解析配置**************/

        /* 指定模块的解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)*/
        /* 若未指定,那么在使用了 --module AMD | System | ES2015 时的默认值为 Classic,其它情况时则为 Node */
        // "moduleResolution": "node",

        /* 在解析非绝对路径模块名的时候的基准路径 */
        // "baseUrl": "./",

        /* 基于 'baseUrl' 的路径映射集合 */
        // "paths": {},

        /* 将多个目录放在一个虚拟目录下,用于运行时 */
        /* 当自己编写的库和开发的代码都输出到一个目录下时,开发代码和库的位置不一样,开发代码引入库的路径就会不对 */
        // "rootDirs": [],
        // "rootDirs": ["src","out"],

        /* 允许 export = xxx 导出 ,并使用 import xxx form "module-name" 导入*/
        // "esModuleInterop": true,

        /* 当模块没有默认导出的时候,允许被别的模块默认导入,这个在代码执行的时候没有作用,只是在类型检查的时候生效 */
        // "allowSyntheticDefaultImports": true,


        /* 不要 symlinks 解析的真正路径 */
        // "preserveSymlinks": true,

        /* 允许在模块中以全局变量的方式访问 UMD 模块内容 */
        // "allowUmdGlobalAccess": true,


        /************** Source Map 配置**************/
        /************** Source Map 配置**************/
        /************** Source Map 配置**************/

        /* 指定 ts 文件位置 */
        // "sourceRoot": "",

        /* 指定 map 文件存放的位置 */
        // "mapRoot": "",

        /* 生成目标文件的 sourceMap */
        // "sourceMap": true,

        /* 将代码与sourcemaps生成到一个文件中,要求同时设置了--inlineSourceMap 或--sourceMap 属性*/
        // "inlineSources": true,

        /* 生成目标文件的 inline sourceMap —— 源文件和 sourcemap 文件在同一文件中,而不是把 map 文件放在一个单独的文件里*/
        // "inlineSourceMap": true,

        /* 生成声明文件的 sourceMap */
        // "declarationMap": true,

        /************** 实验性的配置**************/
        /************** 实验性的配置**************/
        /************** 实验性的配置**************/

        /* 启用装饰器 */
        // "experimentalDecorators": true,

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


        /**************高级配置**************/
        /**************高级配置**************/
        /**************高级配置**************/

        /* 强制区分大小写 */
        // "forceConsistentCasingInFileNames": true

}

    /* 指定需要编译的单个文件列表 */
    // "files": [],

    /* 指定需要编译的文件/目录 */
    // "include": [
    //    // 只写一个目录名等价于 "./src/**/*"
    //    "src"
    //  ]

    /* 需要排除的文件或目录 */
    // "exclude": []

    /* 配置文件继承 */
    // "extends": "./tsconfig.base.json"
}

体会

不要畏惧 TS,别看 TS 官方文档内容很多,其实在项目中常用的都是比较基础的东西,像泛型运用、一些高级类型这种用的很少(封装库、工具函数、UI组件时用的比较多)。只要把常用的东西看熟,一天上手TS。