大量的开源项目都在使用 TypeScript,即便 TypeScript 不能解决问题,也需要先了解它。

据说,

  • TypeScript 主要特性是类型系统,类型检查可以减少运行时错误,有更高的稳定性。
  • 当新人接手 TypeScript 项目时,类型系统更易上手,有更高的可维护性。

类型系统的实际效力,还需要找到量化稳定性和可维护性的方法来验证。

代价是什么?

  • 编译有时耗时很长,会导致 CI/CD 部署比较慢。
  • 如同写测试一样,类型系统也需要更多的开发时间。
  • 需要团队掌握 TypeScript 技术,可能需要前期投入更多学习时间。

原理

TypeScript 是 JavaScript 的超集,ts、tsx 文件需要经过 tsc 编译成 js 文件,才能被浏览器或 node 环境执行。

tsconfig.json 是 TypeScript 的配置文件,rootDir 指定 ts 文件路径,outDir 指定编译后的 js 文件路径。

调试技巧

tsc --watch 可以实时编译。

打开 inlineSourceMap 可以让 dev-tools 打印时显示 ts 文件中对应的行数。

类型推导和断言

let str: string = 'bangbo'
let arr: string[][] = [['a'],['b']]
let tuple: [number, string, boolean] = [1, 'a', true]
function getCode(status: string): number { return 0 }
interface CallOrConstruct {
  new (s: string): Date;  // 构造函数的参数
  (n?: number): number;   // 函数的参数
}

变量,参数和返回值都可以定义类型。当没有定义类型时,TypeScript 也会根据字面量进行类型推导。当类型需要限缩时,也可以使用断言关键字 asis

const a = (expr as Any) as T
const currentStatus: Status = "close" as "close"
const getCode(currentStatus as "close")  // 自动推导为 string,但参数类型不是 string 时需要断言

数据类型

  • any:TypeScript 的重点是「Type」,如果不使用它就是「AnyScript」了,这种情况还不如直接使用 JavaScript。

  • unknown:确保使用该类型的人声明该类型是什么。后端返回的数据不知道数据类型,所以类型为 unknown。处理数据时候,必须使用 as 断言类型,否则类型检查会报错。

  • never:不可能发生这种情况。函数会抛出异常或者分支条件永远为假时,类型为 never。

  • void: 函数没有返回值或者返回 undefind。

New unknown top type

泛型(Generics)

函数和类都可以使用泛型。<> 里面的变量可以使用任意字符串,通常情况下使用 T 表示类型,用 K 表示属性键。

function print<T> (data: T) { console.log('data', data) }

print<number>(999)

class Print<T> {
  data: T
  constructor(d: T) {this.data = d}
}

const p = new Print<number>(999)

条件类型(Conditional Types)

条件类型有助于描述输入和输出的类型之间的关系。

condition ? trueExpression : falseExpression

映射类型(Mapped types)

当需要对对象类型进行复用时,会用到映射类型。映射类型可以定义对象类型的属性键和属性值的类型。

type OnlyBools = { [key: string]: boolean }
// 用例1: 限制子类型的属性键在字面量集合中
type OptionsFlags<Type> = {
  [Property in keyof Type]: boolean;
};
type FeatureFlags = {
  darkMode: () => void;
  newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
// 用例2: 清除属性的 readonly 修饰符
type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// 用例3: 清除属性的可选修饰符
type Concrete<Type> = {
  [Property in keyof Type]-?: Type[Property];
};
// 用例4: 批量修改属性
type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
    name: string;
    age: number;
    location: string;
}
type LazyPerson = Getters<Person>;

索引访问类型(indexed access type)

模板字面量类型(Template Literal Types)

使用模板字符串,简化字符串字面量集合类型的写法。

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

类型操作符

| 是并集,通常用于字面量类型中声明多个特定值。

type status = "open" | "close" | "loding"

& 有两种用法。左右为普通类型时,返回类型交集;左右为对象类型时,合并属性类型。

type T00 = unknown & null; // null
type T01 = { name: string } & { age: number }

extends 返回布尔类型,当操作符左边的类型是右边类型的拓展  时,返回 true。

T extends U ? X : Y
// 用例1: 如果泛型参数 T 是 null 或者 undefined,返回 never,否则返回 T
type NonNullable<T> = T extends null | undefined ? never : T;
// 用例2: 找到 T 和 U 中 T 的差集
type Diff<T, U> = T extends U ? never : T;
// 用例3: ???
type Filter<T, U> = T extends U ? T : never;

extends 操作符右边有时有 infer 关键字,是一种高级用法。(TODO: 补充用例)

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 用例1: 如果泛型参数 T 是
type Func<T> = T extends () => infer R ? R : boolean;
let func3: Func<() => Promise<number>>; // => Promise<number>

keyof 返回对象类型中属性值字面量的集合。类比 JavaScript 中使用 Object.keys 返回属性键列表。

type Staff {
  name: string;
  salary: number;
}
type staffKeys = keyof Staff; // "name" | "salary"
// 用例1: 定义一个函数,返回对象中属性类型
function getProperty<t, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const developer: Staff = {
  name: 'Tobias',
  salary: 100,
};
const nameType = getProperty(developer, 'name'); // string

typeof

instanceof

interface

与 type 最大的不同是可以扩展对象(可写),type 不可以(只读)。

当冒号前加问号时,表示可选属性,即属性类型可以是 undefined。

interface User {
  name: String
  age: number
}
interface User { age?: number }

interface 还可以被类实现。

interface CarProps {
  name: string
  age: number
  start: ()=> void
}

class Car implements CarProps {
  name: string
  age: number

  constructor(name: string, age:number){
    this.name = name
    this.age = age
  }
  start(){}
}

private publice protected

class 中可以用 public private protected 给成员变量设定作用域,但这只是在编码时有效。当成员变量设定为 private 后,无法被外界引用。设定为 protected 后,只能在继承后的类中引用。

另外 JavaScript 中有「真正的」私有变量语法,是在成员变量前加 # 号。

常用 utility

Record 可以指定对象的 key 和 value 的数据类型。

interface CatInfo {
  age: number;
  breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {miffy: { age: 10, breed: "Persian"},
  boris: {age: 5, breed: "Maine Coon"},
  mordred: {age: 16, breed: "British Shorthair"},
};

Pick 可以从 interface 里面挑选几个成员变量。

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  // description: 'sdfd'
};

Omit 用法和 Pick 类型,只是相反,省略几个成员变量。

更多 Unility 查看 TypeScript: Document - Utility Types

枚举

用于增强代码的可读性。通常用于和后端约定返回数据的状态。当不初始化时,数值为从 0 递增的数字。

enum LiveStatus {
    SUCCESS = 0,
    FAIL = -1,
    STREAMING = 1
}

@types

@types 是类型扩充包,开发 TypeScript 项目时,需要添加它们。