大量的开源项目都在使用 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 也会根据字面量进行类型推导。当类型需要限缩时,也可以使用断言关键字 as
或 is
。
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。
泛型(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 项目时,需要添加它们。