TypeScript
背景
TypeScript(简称 TS)是微软开发的一种基于 JavaScript 语言的强类型编程语言。2012年,微软宣布推出 TypeScript 语言,设计者是编程语言设计大师 Anders Hejlsberg。
因为 JavaScript 的弱类型以及特殊运行时语义,随着代码规模和复杂度增加,导致的代码管理难度上升,其中一个普遍性问题就是类型错误,TypeScript 主要目的就是作为 JavaScript 程序的一个静态类型检查器(static typechecker),即一个在代码执行前运行,以确保程序类型是正确的工具。TypeScript 支持所有 JavaScript 特性,并在其上附加了一层类型系统。
静态类型检查(static type-checking)
TypeScript 通过限制类型的变化,以实现类型的静态特征,即变量值类型及对象属性均是静态的。
// 值类型变化报错
let x = 1
x = 'z'
function f ( a:number) {}
f('z')
// 增删属性变化报错
const obj = { x: 1 }
delete obj.x
obj.y = 1
静态类型的优点:
- 有利于发现错误。由于每个值、变量、运算符都有严格的类型约束,不必运行代码,就可以确定变量的类型,从而推断代码有没有拼写、语义和方法调用等错误。
- 更好的 IDE 支持,IDE 可以利用类型信息,提供语法提示功能(编辑器自动提示函数用法、参数等)和自动补全功能(只键入一部分的变量名或函数名,编辑器补全后面的部分)。
- 有助于代码重构。一般地,只要函数或对象的参数和返回值保持类型不变,就能基本确定,重构后的代码也能正常运行。
静态类型的缺点:
- 失去了动态类型代码的灵活性。
- 增加了学习成本和编程工作量。开发者不仅需要编写功能,还需要掌握类型系统,编写类型声明。
- 引入了独立的编译步骤。多出的编译步骤,检查类型是否正确,并将 TypeScript 代码转成 JavaScript 代码。
- 兼容性问题。历史 JavaScript 项目没有做 TypeScript 适配。
类型声明(type annotations)和类型推断(type infer)
TypeScript 的设计思想是,类型声明是可选的,不加类型声明时,TypeScript 会自己推断类型。这种设计好处是可以更好兼容非 TypeScript 代码。
通过范围缩小(narrowing),可以使 TypeScript 从联合类型或 unknown 类型推断出更具体类型。
类型断言 Type Assertions
类型断言要求实际的类型与断言的类型兼容,断言完全无关类型可以先断言成 unknown 类型或 any 类型,然后再断言为目标类型。
expr as unknown as T
as const 可以把内置的基本类型变更为值类型,只能用于字面量,不能用于变量。
unknown 类型
为了解决 any 类型“污染”其他变量的问题,TypeScript 3.0 引入了 unknown 类型。区别:
- unknown 类型的变量,不能直接赋值给除 any 类型和 unknown 类型以外其他类型的变量。
- 不能直接调用 unknown 类型变量的方法和属性。
- unknown 类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、===、!=、!==、||、&&、?)、取反运算(运算符!)、typeof 运算符和 instanceof 运算符这几种,其他运算都会报错。
字面量类型 Literal Types
一个场景是把多个字面量组合成一个联合类型。
TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
大写类型同时包含包装对象和字面量,小写类型只包含字面量,不包含包装对象。
TypeScript 把很多内置方法的参数定义成小写类型,建议使用小写类型。
数组
TypeScript 数组的所有成员的类型必须相同。
// 获取成员类型
type Test = string[]
type TestType = Test[0]
// 等效
type TestType = Test[number]
只读数组
const a:readonly number[] = [1]
// 泛型写法
const a:ReadonlyArray<number> = [1]
const a:Readonly<number[]> = [1]
元组(tuple),成员类型可以自由设置的数组,元组必须明确声明每个成员的类型。
const a:[string, number] = ['z', 1]
成员类型写在方括号里面的就是元组,写在外面的就是数组。
使用扩展运算符(...),可以表示不限成员数量的元组:
type Test = [
string,
...number[]
]
const a:Test = ['z', 1, 2]
函数
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。
根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)。TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型,最后对函数给予完整的类型声明。由于重载是一种比较复杂的类型声明方法,为了降低复杂性,应该优先使用联合类型替代函数重载。
函数类型的对象写法,适合函数本身有属性。
const fn = {
(x: string): number,
y: string
} = f
void 类型允许返回 undefined 或 null。
对象
TypeScript 不区分对象自身的属性和继承的属性,一律视为对象的属性。
如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么可写变量修改属性,会影响到只读变量。
属性名的索引类型,采用属性名表达式的写法来描述多个属性类型。属性索引共有 string、number 和 symbol 三种类型。
type Obj = {
x: string,
[property: string]: string
}
同时有多种类型的属性名索引,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突。
结构类型原则(structual typing),只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,B 是 A 的子类型(subtyping),A 是 B 的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型。为避免子类型异常使用,建议在父类型使用时明确属性使用。
空对象作为类型,其实是 Object 类型的简写形式。所以它不会有严格字面量检查,赋值时总是可以接受各种类型的值,只是不能读取这些属性。
使用没有任何属性的对象:
interface WithoutProperties {
[key: string]: never;
}
接口(interface)
interface 是对象的模板,可以看作是一种类型约定。
// 对象的方法三种写法
interface A {
f(x: boolean): string;
}
interface B {
f: (x: boolean) => string;
}
interface C {
f: { (x: boolean): string };
}
interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。
interface A {
f(): string;
f(x: boolean): boolean;
}
function Fn(): string
function Fn(x: boolean) : boolean
function Fn(x?: boolean): string|boolean {...}
const a:A = {
f: Fn
}
interface 可以使用 extends 关键字,继承其他 interface、class 或 type 命令定义的对象类型,允许多重继承。
同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。例外,同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
interface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: 'div'): HTMLDivElement;
createElement(tagName: 'span'): HTMLSpanElement;
}
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: 'canvas'): HTMLCanvasElement;
}
// 等同于
interface Document {
createElement(tagName: 'canvas'): HTMLCanvasElement;
createElement(tagName: 'div'): HTMLDivElement;
createElement(tagName: 'span'): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
接口与类型别名(Type Aliases) 的区别:
- 同名 interface 会自动合并,同名 type 则会报错。
- interface 不能包含属性映射(mapping),type 可以。
- interface 只能用于对象类型,不能用于原始数据类型。
- this 关键字只能用于 interface。
- interface 无法表达某些复杂类型(比如交叉类型和联合类型),但是 type 可以。
类 class
如果两个地方都设置了只读属性的值,以构造方法为准。
类可以实现多个接口,建议利用继承减少复杂度。
class Test extends A implements B, C {}
TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。
获得一个类的自身类型方式:
// typeof 方式
function createTest (
TestClass: typeof A,
x:number,
):A {
return new TestClass(x)
}
// 构造函数方式,因为 class 本质就是构造函数一种语法糖
interface Test {
new (x:number): A
}
function createTest (
TestClass: new (x:number) => A,
// 或者构造函数的对象方式
// TestClass: Test
x:number
):A {
return new TestClass(x)
}
严格地说,TypeScript private 定义的私有成员,并不是真正意义的私有成员。建议用 ES6 引入的私有成员写法 #propName。
泛型 generics
通过“类型参数”(type parameter),反映参数与返回值之间的类型关系。
习惯上,类型参数的第一个字符采用大写字母。一般使用 T(type 的第一个字母)。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,使用逗号(“,”)分隔。
TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。
function Test<T extends { length: number }, U extends string = 'z'>(
a: T,
b: U
):U {}
枚举 Enum
既是一种类型,也是一个值。适合只关注成员名字的场景,从而增加代码的可读性和可维护性。
由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量。建议在 enum 关键字前面加上 const 修饰,这样编译后代码中 Enum 成员会被替换成对应的值,能提高性能表现。
如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
字符串 Enum 的成员值,不能使用表达式赋值。变量类型如果是字符串 Enum,就不能再赋值为字符串。
模块
显示区分 import/export type 两种写法
import { type A } from './a'
import type { A } from './a'
export { type A }
export type { A }
装饰器 Decorator
用来在定义时修改类(class)的行为,装饰器有如下几个语法特征。
- 第一个字符(或者说前缀)是@,后面是一个表达式。
- @后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
- 这个函数接受所修饰对象的一些相关值作为参数。
- 这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
装饰器函数的类型定义。
type Decorator = (
// 所装饰的对象
value: DecoratedValue,
// 上下文
context: {
// 所装饰对象的类型,六种:'class'、'method'、'getter'、'setter'、'field'、'accessor'
kind: string;
// 所装饰对象的名字
name: string | symbol;
// 函数,用来添加类的初始化逻辑
addInitializer?(initializer: () => void): void;
// 布尔值,表示所装饰的对象是否为类的静态成员
static?: boolean;
// 布尔值,表示所装饰的对象是否为类的私有成员
private?: boolean;
access: {
get?(): unknown;
set?(value: unknown): void;
};
}
) => void | ReplacementValue;
属性修饰符 accessor 等同于为修饰的属性自动生成取值器和存值器
类型运算符
- keyof 运算符,单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
- in 运算符,确定对象是否包含某个属性名,或取出(遍历)联合类型的每一个成员类型。
- 方括号运算符,参数可以是联合类型,也可以是属性名的索引类型。
- extends ? :,三元运算符。
- infer 关键字,定义泛型里面推断出来的类型参数。
- is 运算符,描述函数返回值是 true 还是 false。
- 模板字符串,模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined。
- satisfies 运算符,检测某个值是否符合指定类型。
编译
TypeScript 官方提供的编译器 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本。根据约定,TypeScript 脚本文件使用.ts后缀名,配置文件 tsconfig.json。
tsc 是一个 npm 模块。
$ npm install -g typescript
文章参考