# TypeScript 类型系统

# 1. 基本概念

基本注解:

  • 类型注解使用 TypeAnnotation 语法

  • 类型声明空间中内容都可以用作注解

  • 示例:

    const num: number = 10;
    
    function identity(num: number): number {
      return num;
    }
    

基本类型注解:

  • string / number / boolean

  • 示例:

    let num: number;
    let str: string;
    let bool: boolean;
    

数组注解:

  • 类型[]

  • 示例:

    let boolArr: boolean[];
    

接口注解:

  • 合并多个类型注解

  • 示例:

    interface Name {
      first: string;
      seconde: string;
    }
    
    let name: Name;
    

内联注解:

  • 不用给类型起名字,只用一次

  • 示例:

    let name: {
      first: string;
      seconde: string;
    };
    

特殊类型:

  • any: 关闭类型检查,可赋值给任意类型的变量
  • null / undefined: 可赋值给任意类型的变量
  • void: 表示函数没有返回值

泛型:

  • 示例:

    function reserve<T>(items: T[]): T[] {
      const toreturn = [];
    
      for (let i = items.length - 1; i >= 0; i--) {
        toreturn.push(items[i]);
      }
    
      return toreturn;
    }
    
    const arr = [1, 2, 3];
    
    const reserved = reserve(arr);
    

联合类型注解:

  • 多个类型中挑一个

  • 示例:

    function formatCommandLine(command: string[] | string) {
      // ...
    }
    

交叉类型:

  • 两个类型的并集

  • 示例:

    // first 继承 second 的属性,同名不覆盖
    function extend<T, U>(first: T, second: U): T & U {
      const result = {} as (T & U);
    
      for (let key in first) {
        (result as T)[key] = first[key];
      }
    
      for (let key in second) {
        if (!(result as Object).hasOwnProperty(key)) {
          (result as U)[key] = second[key];
        }
      }
    
      return result;
    }
    
    const x = extend({ a: 1, b: 2 }, { b: '2', c: '3' });
    
    console.log(x); 
    /*
      {
        "a": 1,
        "b": 2,
        "c": "3"
      } 
    */ 
    

元组:

  • [类型1, 类型2, ...]
  • 特殊的数组类型,相当于指定参数列表

类型别名:

  • 给长类型起别名

  • 示例:

    type Text = string | { text: string };
    
    type coordinates = [ number, number ];
    
    type Callback = (date: string) => void;
    

# 2. JS 迁移到 TS

步骤:

  1. 添加 tsconfig.json 文件
  2. .js 文件改为 .ts 文件
  3. 在旧代码中使用类型注解,修复已识别的错误
  4. 为第三方 JS 库定义环境声明

减少错误:

  • 通过 类型断言 减少错误

  • 示例:

    let foo = 123;
    let bar = 'a';
    
    bar = foo as any;
    

第三方 JS 代码:

  • vender.d.ts 为其定义类型

  • 创建针对特定库的声明文件: jquery.d.ts:

    declare type JQuery = any;
    
    // 定义全局变量
    declare var $: JQuery;
    

第三方 NPM 模块:

  • 声明:

    // 定义全局模块
    declare module 'jquery';
    
  • 使用:

    import * as $ from 'jquery';
    

非 JS 资源:

  • 声明:

    declare module '*.css';
    declare module '*.html';
    
  • 使用:

    import * as style from './style/style.css';
    
    import * as app from './template/app.html';
    

# 3. @types

compilerOptions.typeRoots:

  • 默认情况下,所有 node_modules/@types 下的包的类型声明都会加入编译

# 4. 环境声明

为第三方库编写声明文件,通过 declare 关键字声明变量、函数、类等

# 5. 接口

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

class MyPoint implements Point {
  constructor(public x: number, public y: number) {
  }  
}

# 6. 枚举

enum NumEnum {
  first = 1, // 从 1 开始,保证都是真值
  second,
  third,
}

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

// 给枚举添加方法
namespace Weekday {
  export function isBusinessDay(day: Weekday) {
    switch (day) {
      case Weekday.Saturday: return false;
      case Weekday.Sunday: return false;
      default: return true;
    }
  }
}

console.log(Weekday.isBusinessDay( Weekday.Monday ));   // true
console.log(Weekday.isBusinessDay( Weekday.Saturday )); // false

# 7. lib.d.ts

安装 typescript 后,会有 lib.d.ts 文件:

  • 位置:node_modules\typescript\lib\lib.d.ts

  • 内容:

    /// <reference lib="es5" />
    /// <reference lib="dom" />
    /// <reference lib="webworker.importscripts" />
    /// <reference lib="scripthost" />
    

# 8. 函数

重载:

// 重载:
function sum(x: number): number;
function sum(x: number, y: number): number;
function sum(x: number, y: number, z: number): number;

// 实现
function sum(x: number, y?: number, z?: number) {
  if (y == null) {
    y = 0;
  }
  if (z == null) {
    z = 0;
  }
  return x + y + z;
};

console.log(sum(1));
console.log(sum(1, 2));
console.log(sum(1, 2, 3));

声明函数:

type ISum = {
  (x: number): number;
  (x: number, y: number): number;
  (x: number, y: number, z: number): number;
}

type ISum2 = (x: number) => number;

//--

let sum: ISum;

sum = function sum(x: number, y?: number, z?: number) {
  if (y == null) {
    y = 0;
  }
  if (z == null) {
    z = 0;
  }
  return x + y + z;
};

console.log(sum(1));
console.log(sum(1, 2));
console.log(sum(1, 2, 3));

# 9. 可调用

接口:

interface ISum {
  (x: number): number;
}

let sum1: ISum = function (x) {
  return x;
};

箭头函数:

type ISum = (x: number) => number;

let sum1: ISum = function (x) {
  return x;
};

可实例化:

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

  sayHello() {
    console.log(this.name, 'hello');
  }
}

interface IPerson {
  new (name: string): Person;
}

function sayHello(P: IPerson) {
  new P('张三').sayHello();
}

sayHello(Person);

# 10. 类型断言

当 S 是 T 的子集,或 T 是 S 的子集时:

  • S 能被断言为 T

当 S: T | U 时:

  • S 能被断言为 T

双重断言:

function handler(event: Event) {
  // const element = event as HTMLElement; // 错误
  const element = (event as any) as HTMLElement;
}

# 11. freshness

更严格的 对象字面量类型 检查:

function logName(obj: { name?: string }) {
  console.log(obj.name);
}

const person = { name: '张三', age: 18 };
logName(person);

logName({}); // ok
logName({ name: '张三', age: 18 }); // 错误:对象字面量只能指定已知属性 

# 12. 类型保护

typeof:

function doSome(s: number | string) {
  if (typeof s === 'number') { 
    // TS 推导出这个块 s 为 number 类型
    s.toFixed(2);
  } else { 
    // TS 推导出这个块 s 为 string 类型
    s.substring(1, 3);
  }
}

instanceof:

function doStuff(arg: Person | Point) {
  if (arg instanceof Person) {
    // TS 推导出这个块 arg 是 Person 类型
    arg.name;
    return;
  }

  // TS 推导出这个块 arg 是 Point 类型

  arg.x;
}

in:

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

interface IPoint {
  x: number;
}

function doStuff(arg: IPerson | IPoint) {
  if ('name' in arg) {
    // TS 推导出这个块 arg 是 IPerson 类型
    arg.age;
  } else {
    // TS 推导出这个块 arg 是 IPoint 类型
    arg.x;
  }
}

字面量类型:

type IState = 'yes' | 'no';

function doStuff(state: IState) {
  if (state == 'yes') {

  } else if (state == 'no') {
    
  }
}

null 和 undefined:

function doStuff(state?: string | null) {
  if (state == null) {
    // TS 推导出这个块 state 是 null 或 undefined 类型
    return;
  }

  // TS 推导出这个块 state 是 string 类型

  state.substring(1, 2);
}

自定义类型保护:

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

interface IPoint {
  x: number;
}

function isPerson(arg: IPerson | IPoint): arg is IPerson {
  return (arg as IPerson).name !== undefined;
}

function doStuff(arg: IPerson | IPoint) {
  if (isPerson(arg)) {
    // TS 推导出这个块 arg 是 IPerson 类型
    arg.age;
  } else {
    // TS 推导出这个块 arg 是 IPoint 类型
    arg.x;
  }
}

回调函数:

declare var person: { address?: { road: string } };

function doSome(callback: Function) {
  callback();
}

console.log(person.address.road); // 错误: 'person.address' is possibly 'undefined'

if (person.address) {
  console.log(person.address.road);

  var address = person.address;

  doSome(() => {
    console.log(person.address.road); // 错误: 'person.address' is possibly 'undefined'.

    console.log(address.road); // ok
  });
}

# 13. 字面量类型

示例:

type OneOfFive = 1 | 2 | 3 | 4 | 5;
type Bools = true | false;

type CardinalDirection = 'North' | 'East' | 'South' | 'West';

function move(distance: number, direction: CardinalDirection) {
  // ...
}

move(10, 'North');
move(10, 'north'); // 错误

推断:

function sum(num: 100) {
  // ...
}

const num = 100;
sum(num)

const person = { age: 100 };
sum(person.age); // 错误

const person2 = { age: 100 as 100 };
sum(person2.age); // ok

基于字符串的枚举:

function strEnum<T extends string>(arr: T[]): { [key in T]: key } {
  const result = arr.reduce((res, item) => {
    res[item] = item;
    return res;
  }, Object.create(null));

  return result;
}

const Direction = strEnum(['North', 'East', 'South', 'West']);

console.log(Direction);
/*
  {
    "North": "North",
    "East": "East",
    "South": "South",
    "West": "West"
  } 
*/

type IDirection = keyof typeof Direction; // "North" | "East" | "South" | "West"

let d: IDirection;

d = 'West';

d = Direction.West;

# 14. readonly

基本使用:

// 用于接口属性
interface IPerson {
  readonly id: string;
}

// 用于对象成员属性
class Person {
  readonly id = '123';
}

// 用于索引签名
interface Foo {
  readonly [key: string]: string;
}

let p = new Person();
p.id = '1'; // 错误:直接修改会报错


// 间接修改(类型兼容)
function modifyId(p: { id: string }) {
  p.id = '1';
}

modifyId(p);

console.log(p); // { "id": "1" } 

Readonly<T>:

interface IPerson {
  name: string;
  age: string;
}

// 将 IPerson 的所有属性都变为 readonly
type ReadonlyPerson = Readonly<IPerson>;
/* 
type ReadonlyPerson = {
  readonly name: string;
  readonly age: string;
}
*/

ReadonlyArray<T>:

let arr: ReadonlyArray<number> = [1, 2, 3];

arr[0] = 11; // 错误

arr.reverse(); // 变更数组的方法不可用

# 15. 泛型

在成员之间提供类型约束,在参数列表和返回值中同时使用才有意义

function parse<T>(name: T): T {
  return name;
}

parse(1).toFixed(2);

parse('1').substring(1);

# 16. 类型推断

TS 根据一些简单的规则来推断变量的类型:

// 定义变量
// => let num: number
let num = 1; 

// 函数返回值
// => function add(a: number, b: number): number
function add(a: number, b: number) {
  return a + b;
}

// 赋值
type IAdd = (a: number, b: number) => number;
let foo: IAdd = (a, b) => {
  a.toFixed(2); // (parameter) a: number
  return a + b
};

// 解构
let person = { age: 18 };

// => let age: number
let { age } = person;


let nums = ['a', 'b'];

// => let str1: string
let [str1] = nums;

compilerOptions.noImplicitAny:

  • 类型推断为 any 的都会报错

# 17. 类型兼容性

函数-返回值:

let p2 = () => ({ x: 1, y: 2 })
let p3 = () => ({ x: 1, y: 2, z: 3 })

p2 = p3; // 正确: 多的 可以赋值给 少的

p3 = p2; // 错误: 少的 不可以赋值给 多的

函数-参数:

let p2 = (x: number, y: number) => x + y;
let p3 = (x: number, y: number, z: number) => x + y + z;

p2 = p3; // 正确: 多的 不可以赋值给 少的

p3 = p2; // 错误: 少的 可以赋值给 多的

枚举:

enum NumEnum {
  x, y
}

let num = NumEnum.x;
let z = 0;

num = z;
z = num;


enum StrEnum {
  a = 'a', b = 'b'
}

let str = StrEnum.a;
let c = 'c';

str = c; // 错误:string 类型不能赋值给 字符串枚举类型

c = str;

# 18. never

不能优雅退出的函数:

// 没有返回值的函数
function test():never {
  while(true){}
}

// 总是抛出错误的函数
function test2():never {
  throw 'xyz';
}

只有 never 类型的数据才可以赋值给 never 类型的数据:

let x: never = test();

# 19. 辨析联合类型

interface Square {
  kind: 'square';
  size: number;
}

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

type Shape = Square | Rectangle;

function area(s: Shape): number {
  if (s.kind === 'square') {
    return s.size * s.size;
  } else if (s.kind === 'rectangle') {
    return s.width * s.height
  } else {
    const _check: never = s;
    return _check;
  }
}

# 20. 索引签名

声明:

interface INumObj {
  [index: number]: string;
}

let n: INumObj = { 1: 'a' };

interface IStrObj {
  [key: string]: number;
}

let s: IStrObj = { 'a': 1 };

使用字符串字面量:

type props = 'a' | 'b' | 'c';

let obj: { [key in props]: number } = { 
  a: 1, 
  b: 2, 
  c: 3, 
  d: 4 // 错误
  };

嵌套:

type NestedCss = {
  color?: string;
  children?: {
    [key: string]: NestedCss;
  }
}

let css: NestedCss = {
  color: 'red',
  children: {
    '.head': {
      color: 'green'
    },
    '.body': {
      color: 'blue'
    }
  }
}

# 21. 类型移动

复制类型和值:

namespace Address {
  export class Road {}
}

import Road = Address.Road;

let r: Road;

捕获类型:

// 捕获变量的类型
let num = 1;

let n: typeof num; // let num: number


// 捕获类成员的类型
class Person {
  id = '1'
}
declare let p: Person;

let id: typeof p.id; // let id: string


// 捕获字面量类型
const a = 'a'; // const a: "a"

let b = 'b';   // let b: string

let c: typeof a; // let c: "a"

捕获对象的键名:

const person = {
  name: '张三',
  age: 18,
};

// type PersonType = { name: string; age: number; }
type PersonType = typeof person;

// type PersonKeysType = "name" | "age"
type PersonKeysType = keyof PersonType;

type KeysType = keyof typeof person; // "name" | "age"

# 22. 异常处理

抛出异常: throw new Error('error message')

捕获异常: try {} catch(e) {}

错误子类型:

  • RangeError: 数字或参数 超出有效范围
  • ReferenceError: 使用未定义的变量
  • SyntaxError: 语法错误
  • TypeError: 类型错误,如 在字符串类型变量上调用数字类型的方法
  • URIError: decodeURI 、encodeURI 传入无效参数时

避免抛出非 Error 类型的东西:

throw '这是一个错误'; // 不推荐

throw new Error('这是一个错误'); // 使用 Error 能自动跟踪堆栈和错误位置

通过回调返回错误:

function doSome(content: string, callback: (e?: Error) => void) {
  if (content == null) {
    callback(new Error('content 不能为空'));
    return;
  }

  callback()
}

不返回错误对象:

function validate(value: number): { error?: string } {
  if (value < 100) {
    return { error: 'value is less than 100' };
  }

  return {}
}

# 23. ThisType

指定函数、方法的 this :

let person = {
  name: '张三',
}

function sayHello(this: typeof person, age: number) {
  console.log(`hello, I'm ${ this.name }, ${ age } years old.`)
}

sayHello.bind(person)(18)

ThisType<T>:

type ObjectDescriptor<TData, TMethods> = {
  data?: TData;
  methods?: TMethods & ThisType<TData & TMethods>
};

function makeObject<TData, TMethods>(desc: ObjectDescriptor<TData, TMethods>): TData & TMethods {
  let data: Object = desc.data || {};
  let methods: Object = desc.methods || {};

  return { ...data, ...methods } as (TData & TMethods);
}

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

obj.moveBy(10, 20);

console.log(obj);
/*
  {
    "x": 10,
    "y": 20
  }
*/
本章目录