typescript系列-typescript入门

官方网站:https://www.typescriptlang.org/zh/

官网文档:https://www.typescriptlang.org/zh/docs/https://www.tslang.cn/docs/home.html

入门教程:https://ts.xcatliu.com/

中文文档:https://zhongsp.gitbooks.io/typescript-handbook/content/

中文手册:https://typescript.bootcss.com/tutorials/typescript-in-5-minutes.html

相关文档:

react 支持 ts:https://typescript.bootcss.com/tutorials/react.html

相关书籍:

深入理解 TypeScript:https://jkchao.github.io/typescript-book-chinese/#why

相关工具:

Typescript AST Viewer:https://ts-ast-viewer.com/

入门(适合0基础):

typescript in 5 minutes https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html

进阶(适合有一定基础):

Deep Dive into Typescript(深入理解Typescript) https://basarat.gitbook.io/typescript/

中文版(可能有些落后):https://jkchao.github.io/typescript-book-chinese/

在项目中使用ts前必读(不管你有没有基础):

react typescript cheatsheet https://react-typescript-cheatsheet.netlify.app/docs/basic/setup

总结性文章(进阶推荐!!!非常实用):TypeScript 类型编程: 从基础到编译器实战 - 抖音前端

TypeScript 是什么

JavaScript 的超集。TypeScript = JavaScript + 静态类型系统

TypeScript 可以在运行代码之前找到错误并提供修复

TypeScript 是结构类型系统,类型之间的对比只会比较它们最终的结构,而会忽略它们定义时的关系。

需要编译成 JavaScript 才能在浏览器和 node 环境(任何可以运行JavaScript的环境)中运行。

优势

  • 开发过程中及时发现潜在问题
  • 代码语义更清晰易懂

解决了

多人开发时变量类型混乱,语义混乱等问题

在开发过程中无法排查的类型引用等问题

环境

  • node环境
  • 安装 ts-node (npm i ts-node -g) 对 ts 文件进行编译 tsc xx.ts 和运行 ts-node xx.ts
  • 安装 typescript (npm i typescript -g)
  • 配置 tsconfig.json

ts-node 与 tsc (typescript 自带的编译方式)编译的区别

tsc根据 tsconfig 进行编译所有文件,ts-node 将从入口文件开始,并根据导入 / 导出逐步在树中转移文件。

局部环境

# 初始化项目
npm init -y

yarn add ts-node typescript -D

# 初始化ts
npx tsc --init

工具

nodemon:nodemon是一种工具,可在检测到目录中的文件更改时通过自动重新启动节点应用程序来帮助开发基于node.js的应用程序。

concurrently:并行解决方案。concurrently npm:dev:*

parcel:web应用打包工具

常见概念

静态类型定义:string、number、boolean、object/{}、Array<元素类型>/元素类型[]、any、void。

类型注解:TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。 TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。

类型推断:TypeScript 会自动进行推断没有定义类型注解的变量类型。

类型保护:判断数据类型来做相应的操作,方法有:as 断言、in 检查是否存在、typeof 和 instanceof 检查数据类型

// as 断言
// in 方法

注意点

  • 尽管有错误,js文件还是会被创建了。
  • this 没有隐式的 any

基础类型

// boolean
let isDone: boolean = false

// number
let num: number = 1

// string
let str: string = 'xxx'

// array
let numList: number[] = [1, 2]
let strList: Array<string> = ['1', '2']

// 元组 tuple: 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。
// 当访问越界元素,会使用联合类型替代
let arrTuple: [string, number]
arrTuple = ['x', 2]
arrTuple = 'y' // true 字符串可以赋值给(string | number)类型

// 枚举 enum: enum类型是对JavaScript标准数据类型的一个补充。
// 默认情况下,从0开始为元素编号。也可以手动的指定成员的数值。
enum Color {
    Red,
    Green
}
let color: Color = Color.Red // 0
let colorName: string = Color[1] // Green

// any: 未知类型,可以用来指定动态内容
let anyValue: any = 4
anyValue = '4'

// void: 表示没有任何类型。当一个函数没有返回值时可以使用。
function warnUser(): void {
    console.log("This is my warning message");
}
let unusable: void = undefined; // 只能赋予undefined和null

// null | undefined: 默认情况下null和undefined是所有类型的子类型。 就是说你可以把 null和undefined赋值给number类型的变量。

// never: 表示的是那些永不存在的值的类型。
// never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。
// never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never。
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

// object: 表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。
// Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法。
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
// 使用object类型,就可以更好的表示像Object.create这样的API。
declare function create(o: object | null): void;
create({ prop: 0 }); // OK

对象类型

属性设置
interface Person {
    readonly id: numbser; // 只读项
    name: string;
    age?: number; // 可选项
    [propName: string]: any; // 表示可以传入其他属性
}

不设置可传入其他属性,当函数传参是对象时,如果传入的是字面量定义的对象则会进行强校验,有多余未注解属性则会报错,而如果是变量定义的对象则不会。

函数类型

注解函数参数和返回值类型
// 方法一
const func1 = ( str: string ): number => {
    return parseInt(str)
}
// 方法二
const func2: ( str: string ) => number = (str) => {
    return parseInt(str)
}
// 方法三 利用类型推断
const func1 = ( str: string ) => {
    return parseInt(str)
}
使用解构参数
function add(
	{ first, second }: { first: number, second: number }
): number {
    return first + second
}

const total = add({ first: 1, second: 2 })
可选参数
function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

可选参数后不能再出现必选参数。

用接口定义函数
interface SearchFunc {
    (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    return source.search(subString) !== -1;
}

采用函数表达式|接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

重载

重载允许一个函数接受不同数量或类型的参数时,做出不同的处理。

function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

枚举类型(enum)

枚举(Enum)类型用于取值被限定在一定范围内的场景。

枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:

enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true

泛型

参考链接:https://ts.xcatliu.com/advanced/generics.html

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

// T 仅表示占位符 可以使用其他字符表示
function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']
约束泛型
// 约束泛型
interface WithLength {
  length: number
}
// 使用 extends 进行约束条件
function echoWithLength<T extends WithLength>(arg: T): T {
  console.log(arg.length)
  return arg
}
const str = echoWithLength('str')

// 在约束泛型中使用类型参数
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
函数使用泛型
function identity<T>(arg: T): T {
  return arg;
}
 
let myIdentity: <Input>(arg: Input) => Input = identity;

// 使用对象声明类型
let myIdentity: { <Type>(arg: Type): Type } = identity;

// 使用 interface 声明类型
interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;
泛型递归调用
// 泛型调用支持递归
type RecursiveGenerics<T> = T extends string ? T : RecursiveGenerics<T>;

// 比如类型对象的深度优先遍历、实现循环等等
// 例如 斐波那契数列
// 辅助函数,暂时不用关心
type NumberToArray<T, I extends any[] = []> = T extends T ? I['length'] extends T ? I : NumberToArray<T, [any, ...I]> : never;
type Add<A, B> = [...NumberToArray<A>, ...NumberToArray<B>]['length']
type Sub1<T extends number> = NumberToArray<T> extends [infer _, ...infer R] ? R['length'] : never;
type Sub2<T extends number> = NumberToArray<T> extends [infer _, infer __, ...infer R] ? R['length'] : never;

// 计算斐波那契数列 使用条件类型判断边界
type Fibonacci<T extends number> = 
  T extends 1 ? 1 :
  T extends 2 ? 1 :
  Add<Fibonacci<Sub1<T>>, Fibonacci<Sub2<T>>>;
  
  type Fibonacci9 = Fibonacci<9>;
  /** 得到结果
  type Fibonacci9 = 34
  */
类使用泛型
// 类使用泛型
class Queue<T> {
  private data: T[] = [];
  push(item: T): void {
    this.data.push(item)
  }
  pop(): T | undefined {
    return this.data.shift()
  }
}
const queue = new Queue<number>()
queue.push(1)
console.log(queue?.pop()?.toFixed())

类具有两种类型数据:实例数据和静态数据。泛型只能定义实例数据的类型,静态数据不能使用类的类型参数。

泛型类

当我们创建一个工厂函数的时候,有必要参考类的构造函数。

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

利用原型属性进行类型推断和约束类的实例属性的类型。

class BeeKeeper {
  hasMask: boolean = true;
}
 
class ZooKeeper {
  nametag: string = "Mikle";
}
 
class Animal {
  numLegs: number = 4;
}
 
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
 
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
 
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

类型断言

可以用来手动指定一个值的类型。

类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除。

形式:

let someValue: any = "this is a string";

// 形式一 尖括号 语法
let strLength: number = (<string>someValue).length;

// 形式二 as 语法 jsx中只能使用这种方式(推荐使用)
let strLength: number = (someValue as string).length;
应用场景

将一个联合类型断言为其中一个类型。要使用联合类型中的某个类型特定的属性时,如果直接判断则会报错,所以可以使用类型断言,断言成有用该特定属性的类型。

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。

使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

将一个父类断言为更加具体的子类。

interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}

将任何一个类型断言为any。

如果向 window 对象上添加一个属性,但是取值的时候 ts 会判断错误,这时可以将 window 临时断言为 any 类型。将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。

但是这样会带来一些问题,一方面不能滥用 as any,它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡

将 any 断言为一个具体的类型。弥补旧代码中的 any 类型,提高代码的可维护性。

function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

以上代码可以使用 泛型 更加规范的约束

function getCacheData<T>(key: string): T {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData<Cat>('tom');
tom.run();
类型断言的限制
  • 联合类型可以被断言为其中一个类型
  • 父类可以被断言为子类
  • 任何类型都可以被断言为 any
  • any 可以被断言为任何类型
  • 要使得 A 能够被断言为 B,只需要 A 兼容 BB 兼容 A 即可
类型断言 & 类型声明
interface Animal {
    name: string;
}
interface Cat {
    name: string;
    run(): void;
}

const animal: Animal = {
    name: 'tom'
};
// 类型断言
let tom = animal as Cat;

// 类型声明
let tom: Cat = animal;
// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.

核心区别:

  • animal 断言为 Cat,只需要满足 Animal 兼容 CatCat 兼容 Animal 即可
  • animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行

所以,类型声明比类型断言更加严格,因此优先使用类型声明

类型别名

类型别名用来给一个类型起个新名字。

通常注解对象类型和联合类型时使用。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    } else {
        return n();
    }
}

// 函数
type PlusType = (x: number, y: number) => number
function sum(x: number, y: number) : number {
    return x + y
}
const sum2: PlusType = sum

扩展

type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

const bear = getBear();
bear.name;
bear.honey;

interface 接口

在 TypeScript 里,只在两个类型内部的结构兼容那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用 implements语句。

注解对象中的属性或注解函数。

接口之间可以进行继承(extends)。

interface Person {
    readonly id: numbser; // 只读项
    name: string;
    age?: number; // 可选项
    [propName: string]: any; // 表示可以传入其他属性 泛匹配
}

// 扩展
interface Teacher extends Person {
    teach(): string;
}

interface say {
    (word: string): string
}
类型别名(type) 和 interface 的区别
  • 类型别名可以直接重新定义基础类型名称,而 interface 不可以,只能设置对象类型。

  • 实战中,interface 比类型别名更加常用,如果无法使用 interface,则使用类型别名。

  • interface 可以重复声明,但是类型别名不能重复声明。

  • interface 可以进行声明合并,但是类型别名不可以。

  • interface 只能声明对象属性组成,但是不能声明基本类型。

只能使用 interface 不能使用 type 的情况

往函数上挂载属性。

interface FuncWithAttachment {
  (param: string): boolean;
  someProperty: number;
}

const testFunc: FuncWithAttachment = {};
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3; // 有类型提醒
声明合并
interface Alarm {
    price: number;
}
interface Alarm {
    weight: number;
}

// 相当于
interface Alarm {
    price: number;
    weight: number;
}

注意:合并的属性的类型必须是唯一的,属性类型不一致

class 类

implements:class 类应用 interface(接口) 。

class User implements Person {
    name = 'xx'
}
构造函数
class Person {
    public name: string;
    constructor(name) {
        this.name = name
    }
}

// 等价于 简化写法
class Person {
    constructor(public name: string) {}
}
readonly
class Person {
    public readonly name: string;
    constructor(name: string) {
        this.name = name;
    }
}
抽象类

abstract 关键字定义抽象类

抽象类只能被继承不能被实例化。子类必须要实现抽象类中定义的抽象方法。

abstract class Geom { // 定义抽象类
    width: number;
    getType() {
        return 'Gemo'
    }
    abstract getArea(): number; // 定义抽象方法
}

class Circle extends Geom {
    getArea() {
        return 123;
    }
}
单例模式

只生成一个对象。

class Demo {
    private static instance: Demo;
    private constructor(public name: string) {}
    
    static getInstance() {
        !this.instance && (this.instance = new Demo('xxxx'))
        return this.instance;
    }
}

// 使用
const demo1 = Demo.getInstance()
const demo2 = Demo.getInstance()
console.log(demo1.name)
console.log(demo2.name)

类型操作符

&(对象类型合并)、|(联合类型)

keyof 的使用

获取对象类型的键。结合类型映射会非常好用。

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

class Teacher {
    constructor(private info: Person) {}
    getInfo<T extends keyof Person>(key: T): Person[T] {
        return this.info[key];
    }
}
// keyof 循环类似于 type类型别名
// type T = 'name';
// key: 'name';
// Person['name'];

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number;

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
// type M = string | number;

// ===================================
type t<T> = keyof T;

interface Test {
  a: string;
  b: string;
}

type TestType = t<Test>; // type TestType = 'a' | 'b'

const test: TestType = 'a';

注意 keyof 只能对类型使用,如果想要对值使用,需要先使用 typeof 获取类型。

typeof 关键字

获取值的类型。

const Test = {
  a: '1',
  b: 2,
}
type TestType = typeof Test;

const test: TestType = {
  a: '3',
  b: 4,
};

// 搭配 ReturnType<T> 使用
function f() {
  return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>; 
    // type P = {
    //     x: number;
    //     y: number;
    // }

注意对于 enum 需要先进行 typeof 操作获取类型,才能通过 keyof 等类型操作完成正确的类型计算(因为 enum 可以是类型也可以是值,如果不使用 typeof 会当值计算):

enum E1 {
  A,
  B,
  C
}

type TE1 = keyof E1;
/**
拿到的是错误的类型
type TE1 = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
*/

type TE2 = keyof typeof E1;
/**
拿到的是正确的类型
type TE2 = "A" | "B" | "C"
*/

获取类型中的某个属性类型

// 常规用法
type Person = { age: number; name: string; alive: boolean }; // 换成 interfafce 也适用
type Age = Person["age"]; // type Age = number;

// 使用 union / keyof / others type
type I1 = Person["age" | "name"]; // type I1 = string | number
 
type I2 = Person[keyof Person]; // type I2 = string | number | boolean
 
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName]; // type I3 = boolean | string

// 使用 typeof 数字 获取数组中的子元素类型
const MyArray = [
  { name: "Alice", age: 15 },
  { name: "Bob", age: 23 },
  { name: "Eve", age: 38 },
];
 
type Person = typeof MyArray[number];      
// type Person = {
//     name: string;
//     age: number;
// }
type Age = typeof MyArray[number]["age"]; // type Age = number
type Age2 = Person["age"]; // type Age2 = number

in 遍历对象键值

// 下述示例遍历 '1' | '2' | 3' 三个值,然后依次赋值给 K,K 作为一个临时的类型变量可以在后面直接使用 
/**
下述示例最终的计算结果是:
type MyType = {
    1: "1";
    2: "2";
    3: "3";
}
因为 K 类型变量的值在每次遍历中依次是 '1', '2', '3' 所以每次遍历时对象的键和值分别是 { '1': '2' } { '2': '2' } 和 { '3': '3' },
最终结果是这个三个结果取 &
*/
type MyType = {
  // 注意能遍历的类型只有 string、number、symbol,也就是对象键允许的类型
  [K in '1' | '2' | '3']: K 
}

[in] 常常和 keyof 搭配使用,遍历某一个对象的键,做相应的计算后得到新的类型,如下:

type Obj = {
  a: string;
  b: number;
}
/**
遍历 Obj 的所有键,然后将所有键对应的值的类型改成 boolean | K,返回结果如下:
type MyObj = {
    a: boolean | "a";
    b: boolean | "b";
}
这样我们就实现了给 Obj 的所有值的类型加上 | boolean 的效果
*/
type MyObj = {
  [K in keyof Obj]: boolean | K
}

in 后面还可以接 as,as 后面可以接类型表达式(文档:https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as)

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

条件语句

// 条件表达式 类型表达式1 extends 类型表达式2 ? 类型表达式 : 类型表达式
SomeType extends OtherType ? TrueType : FalseType;

// 使用 泛型 进行扩展
type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

// 条件类型约束
type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>; // type EmailMessageContents = string

// 移动约束 和 条件约束
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
Flatten
type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extracts out the element type.
type Str = Flatten<string[]>; // type Str = string
 
// Leaves the type alone.
type Num = Flatten<number>; // type Num = number
推断扁平的数据类型

改进上一步 flatten

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type T = Flatten<string[]>; 
/* T = string, 因为推断出 string[] = Array<string>,所以 Item = string,类型返回 Item */

注意:infer 只能在条件类型里面使用

infer 推断类型

infer 关键词:https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

可以使用 infer 关键字推断条件类型中的某一个条件类型,然后将该类型赋值给一个临时的类型变量。
类型推断可以用于 extends 后任何可以使用类型表达式的位置。

可以通过 infer 关键字实现很多内置类型的操作:

// 自动推断参数 P 的类型,如果是则泛型返回值是 P
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
/**
推断参数的类型成功
type Params = [a: string, b: number]
*/
type Params = MyParameters<(a: string, b: number) => void>;

// 同样的方式,我们可以推断 ReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
/**
团队返回值的类型成功
type Ret = void
*/
type Ret = MyReturnType<(a: string, b: number) => void>;

infer 还可以和元组或者模版字符串结合:

// 计算元组中的第一个元素
type Head<T extends any[]> = T extends [infer F, ...infer R] ? F : never;

// 解析 `1 + 2 + 3` 形式的字符串,并返回 AST
// T extends ${infer ExpressionA} + ${infer ExpressionB},如果字符串满足 A + B 的模式,即可通过 infer 推断出 A 和 B 的字符串
type Parse<T extends string> = T extends `${infer ExpressionA} + ${infer ExpressionB}` ? {
  type: 'operator',
  left: Parse<ExpressionA>,
  right: Parse<ExpressionB>
}: {
  type: 'expression',
  value: T
};

进阶

动态扩展类型变量

T 中可保存上一次调用 option 后的值,然后通过类型递归,扩展 T 的类型,当最后调用 get() 时,拿到的就是扩展后的 T 的类型:

type Chainable<T = {}> = {
  option<K extends string, V extends any>(key: K, value: V): Chainable<T & { [key in K]: V }>
  get(): T
}

使用了默认泛型 + 递归两个特性,利用递归保存上下文,就可以实现对已有类型变量的扩展。

利用这个特性可以保存链式调用中的上下文。

动态更改对象类型的 key

通过 key in keyof T as xxx形式可以重写 key。可以通过这种形式来实现动态更改对象类型的 key,比如实现 OptionalKeys 或者 RequiresKeys 或者 ReadonlyKeys

type IsOptional<T, K extends keyof T> = Partial<Pick<T, K>> extends Pick<T, K> ? true : false;

type OptionalKeys<T> = keyof {
  [K in keyof T as IsOptional<T, K> extends true ? K : never]: T[K];
};

type RequiredKeys<T> = {
  [K in keyof T]: IsOptional<T, K> extends true ? never : T[K]
}

这里注意 as 后面可以接一个类型表达式,我们可以通过临时变量 K 以及辅助的类型表达式,实现对键的复杂的操作,比如增加、删除特定的键,将特定的键标记为可选,将特定的键标记为 readonly 等等。

上述根据条件将 K 的类型重写为 never 可以去掉该 key,但是注意将值的返回类型设置成 never 是无法更改 key 的数量的,如下:

type RequiredKeys<T> = {
  [K in keyof T]: IsOptional<T, K> extends true ? never : T[K]
}

返回的 never 值将会变为 undefined。

构造器

文档:https://www.tslang.cn/docs/handbook/decorators.html

属性构造器,可以返回一个 propertyDescriptor 去修改属性的读写能力。

function nameDecorator(target: any, key: string): any { 
    // 返回值必须是 void 或 any
    // target指向的是类的原型prototype 而不是实例 无法对属性值做修改
    const descriptor: PropertyDescriptor = {
        writable: false
    }
    return descriptor
}

class Test {
    @nameDecorator
    name = 'xxx'
}

const test = new Test()

reflect 拦截

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它。

使用库:reflect-metadata

参考文章:

https://juejin.cn/post/6844904152812748807

https://jkchao.github.io/typescript-book-chinese/tips/metadata.html

声明文件

参考文章:https://ts.xcatliu.com/basics/declaration-files.html

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。

xxx.d.ts

// 定义全局变量
declare var $: (params: () => void) => void;

// 定义全局函数
interface JqueryInstance {
    html: (html: string) => JqueryInstance;
}
// 函数重载
declare function $(readyFunc: () => void): void;
declare function $(selector: string): JqueryInstance;
// 对对象进行类型定义,以及对类进行类型定义、命名空间的嵌套  [使用$.fn.init()]
declare namespace $ {
    namespace fn {
        class init {}
    }
}

// 使用 interface 实现函数重载
interface Jquery {
    (readyFunc: () => void): void;
    (selector: string): JqueryInstance;
}

declare var $: Jquery;

ES6 module处理

declare module 'jquery' {
    interface JqueryInstance {
        html: (html: string) => JqueryInstance;
    }
    // 混合类型
    function $(readyFunc: () => void): void;
    function $(selector: string): JqueryInstance;
    namespace $ {
        namespace fn {
            class init {}
        }
    }
        
    export = $;
}

配置

  • noImplicitReturns :会防止你忘记在函数末尾返回值。
  • noFallthroughCasesInSwitch :会防止在switch代码块里的两个case之间忘记添加break语句。
  • noImplicitAny :防止将没有明确指定的类型默默地推断为 any类型。
  • noEmitOnError

TypeScript还能发现那些执行不到的代码和标签,你可以通过设置allowUnreachableCodeallowUnusedLabels选项来禁用。

实战

注意事项

  • 应该聪明地使用类型断言(as)
    • 什么时候应该使用as:当我比ts编译器能够更精确地确定类型的时候(最好使用type guards)
    • 什么时候不应该使用as:当我也不知道咋回事,只是想让ts类型检查不报错的时候
  • 好的typescrip代码是不怎么需要写类型的,要充分利用typescript的类型推导能力(流动的类型)
  • 好的typescript代码是不需要写各种判断的,要充分利用typescript的类型组合能力,也就是所谓的:好的类型应该是更紧的类型

问题

类型描述文件

当 ts 文件引入 js 库时,需要使用 .d.ts 翻译文件。翻译文件会帮助 ts 文件引入 js 文件。

安装 .d.ts 文件,npm i @types/xxx -D

类型描述文件不准确

使用 interface extends 方法进行扩展属性。

如何动态改变数据类型

如使用 express 中间件对 require 和 response 做了修改,但是在初始声明类型时是 express 自带的类型,没有新增的属性声明。

使用类型融合的方法,创建一个新的 .d.ts 文件扩展原框架中的类型定义,ts 底层会自动进行 mixin。

关于使用 interface 还是 type

Use Interface until You Need Type - orta.

More Advice

Here’s a helpful rule of thumb:

  • always use interface for public API’s definition when authoring a library or 3rd party ambient type definitions, as this allows a consumer to extend them via declaration merging if some definitions are missing.
  • consider using type for your React Component Props and State, for consistency and because it is more constrained.

You can read more about the reasoning behind this rule of thumb in Interface vs Type alias in TypeScript 2.7.

The TypeScript Handbook now also includes guidance on Differences Between Type Aliases and Interfaces.

Note: At scale, there are performance reasons to prefer interfaces (see official Microsoft notes on this) but take this with a grain of salt

Types are useful for union types (e.g. type MyType = TypeA | TypeB) whereas Interfaces are better for declaring dictionary shapes and then implementing or extending them.

什么时候推荐用 type 什么时候用 interface ?

大多数情况下使用 interface,而不是 type

扩展

ts 工具类型:https://www.typescriptlang.org/docs/handbook/utility-types.html