Typescript 简介
Typescript(Typed JavaScript at Any Scale) = Type + ECMAScript + Babel-Lite
- ECMAScript 的超集 (支持ES6)
- 编译期的类型检查
- 不引入额外开销(零依赖,不扩展 js 语法,不侵入运行时)
-
为什么使用 Typescript
增加了代码的可读性和可维护性
- 减少运行时错误,写出的代码更加安全,减少 BUG
- 享受到代码提示带来的好处
-
基础类型
boolean
- number
- string
- array
- tuple
- enum
- void
- null & undefined
- any & unknown
-
any和unknown的区别 any: 任意类型unknown: 未知的类型
任何类型都能分配给 unknown,但 unknown 不能分配给其他基本类型;而 any 啥都能分配和被分配。
在静态编译的时候,unknown 不能调用任何方法,而 any 可以。
let foo: unknownlet bar: anyfoo = true // okfoo = 123 //okbar = true // okbar = 123 //okfoo.toFixed(2) // error: foo是unknown类型,unknown类型静态检查不通过报错(foo as number).toFixed(2) // ok: foo类型断言为为numberbar.toFixed(2) // ok: any类型相当于放弃了静态检查let foo1: string = foo // error: unknown 不能分配给其他基本类型let bar1:string = bar // ok: any类型相当于放弃了静态检查
可以看到,用了 any 就相当于完全丢失了类型检查,所以尽量少用 any,在很多场景下,对于未知类型可以用 unknown,它可以替代 any 的功能同时保留静态检查的能力。
unknown 的正确用法
我们可以通过不同的方式将 unknown 类型缩小为更具体的类型范围:
function getLen(value: unknown): number {if (typeof value === 'string') {// 因为类型保护的原因,此处 value 已经被识别为 string 类型return value.length}return value.length // Error: 这里的input还是unknown类型,静态检查报错。}
这个过程叫类型收窄(type narrowing)。
void
在 TS 中,void 和 undefined 功能高度类似,可以在逻辑上避免不小心使用了空指针导致的错误。
function foo() {} // 这个空函数没有返回任何值,返回类型缺省为voidconst a = foo(); // 此时a的类型定义为void,你也不能调用a的任何属性方法
void 和 undefined 类型最大的区别是,可以理解为 undefined 是 void 的一个子集,当对函数返回值并不在意时,返回什么类型都行或不返回,使用 void 而不是 undefined。举一个 React 中的实际的例子。
// Parent.tsxfunction Parent(): JSX.Element {const getValue = (): number => { return 2 }; /* 这里函数返回的是number类型 */// const getValue = (): string => { return 'str' }; /* 这里函数返回的string类型,同样可以传给子属性 */return <Child getValue={getValue} />}// Child.tsxtype Props = {getValue: () => void; // 这里的void表示逻辑上不关注具体的返回值类型,number、string、undefined等都可以}function Child({ getValue }: Props) => <div>{getValue()}</div>
never
never 一般表示没法正常结束返回的类型,无法达到的类型。一般用在必定会报错或者死循环的函数,让代码无法继续往下执行。
// never 用户控制流分析function neverReach (): never {throw new Error('an error')}// 在一个函数中调用了返回 never 的函数后,之后的代码都会变成deadcodeconsole.log(111); // Error: 编译器报错,此行代码永远不会执行到// 这个死循环的也会无法正常退出function foo(): never {while(true){}}// Error: A function returning 'never' cannot have a reachable end point.function foo(): never {let count = 1;while(count){ count ++; }}
never还可以是永远没有相交的类型
type human = 'boy' & 'girl' // 这两个单独的字符串类型并不可能相交,故human为never类型
never 还可以用于联合类型的 幺元,即任何类型联合上 never 类型,还是原来的类型:
幺元即单位元,在加法里对应0,乘法里对应1,不影响结果。
type T0 = string | number | never // T0 is string | number
无法把其他类型赋给 never,包括any。 所以never的一个hack用法:
interface Foo {type: 'foo'}interface Bar {type: 'bar'}type All = Foo | Bar// 在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):function handleValue(val: All) {switch (val.type) {case 'foo':// 这里 val 被收窄为 Foobreakcase 'bar':// val 在这里是 Barbreakdefault:// val 在这里是 neverconst exhaustiveCheck: never = valbreak}}// 若是修改了All类型,并且忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,此时default里的val会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。type All = Foo | Bar | Baz
函数类型
返回值类型写法
function fn(): number {return 1}const fn = function (): number {return 1}const fn = (): number => {return 1}const obj = {fn (): number {return 1}}
在
()后面添加返回值类型即可。
函数类型
ts 中也有函数类型,用来描述一个函数:
type FnType = (x: number, y: number) => number
完整的函数写法
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {return x + y}// 使用 FnType 类型let myAdd: FnType = function(x: number, y: number): number {return x + y}// ts 自动推导参数类型let myAdd: FnType = function(x, y) {return x + y}
函数重载
js 因为是动态类型,本身不需要支持重载,ts 为了保证类型安全,支持了函数签名的类型重载。即:
多个**重载签名**和一个**实现签名**
// 重载签名(函数类型定义)function toString(x: string): string;function toString(x: number): string;// 实现签名(函数体具体实现)function toString(x: string | number) {return String(x)}let a = toString('hello') // oklet b = toString(2) // oklet c = toString(true) // error
如果定义了**重载签名**,则**实现签名**对外不可见
function toString(x: string): string;function toString(x: number): string {return String(x)}len(2) // error
**实现签名**必须兼容**重载签名**
function toString(x: string): string;function toString(x: number): string; // error// 函数实现function toString(x: string) {return String(x)}
**重载签名**的类型不会合并
// 重载签名(函数类型定义)function toString(x: string): string;function toString(x: number): string;// 实现签名(函数体具体实现)function toString(x: string | number) {return String(x)}function stringOrNumber(x): string | number {return x ? '' : 0}// input 是 string 和 number 的联合类型// 即 string | numberconst input = stringOrNumber(1)toString('hello') // oktoString(2) // oktoString(input) // error
运算符
非空断言运算符 !
!运算符可以用在变量名或者函数名之后,用来强调对应的元素是非 null | undefined 的。
function onClick(callback?: () => void) {callback!(); // 参数是可选的,加了这个感叹号!之后,TS编译不报错}
这个符号的场景,特别适用于已经明确知道不会返回空值的场景,从而减少冗余的代码判断,如 React 的 Ref:
function Demo(): JSX.Elememt {const divRef = useRef<HTMLDivElement>();useEffect(() => {divRef.current!.scrollIntoView(); // 当组件Mount后才会触发useEffect,故current一定是有值的}, []);return <div ref={divRef}>Demo</div>}
可选链运算符?.
?.是开发者最需要的运行时(当然编译时也有效)的非空判断。
obj?.prop obj?.[index] func?.(args)
?.用来判断左侧的表达式是否是 null | undefined,如果是则会停止表达式运行,可以减少我们大量的&&运算。
比如a?.b 会被编译成 a === null || a === void 0 ? void 0 : a.b;
这里涉及到一个小知识点:undefined这个值在非严格模式下会被重新赋值,使用void 0必定返回真正的 undefined。
空值合并运算符??
?? 与 || 的功能是相似的,区别在于??在左侧表达式结果为 null 或者 undefined 时,才会返回右侧表达式。比如let b = a ?? 10,生成的代码如下:
let b = a !== null && a !== void 0 ? a : 10;
而 || 表达式,对 false、''、NaN、0等逻辑空值也会生效,不适于做对参数的合并。
数字分隔符_
_可以用来对长数字做任意的分隔,主要设计是为了便于数字的阅读,编译出来的代码是没有下划线的,请放心食用。
let num:number = 1_2_345.6_78_9
操作符
键值获取keyof
keyof是一个类型关键词 ,可以获取一个类型所有key值,返回联合类型,语法格式类型 = keyof 类型:
interface Person {name: stringage: number}type PersonAttrs = keyof Person // 'name' | 'age'type Person = {name: string;age: number;}type PersonKey = keyof Person; // 'name' | 'age'
keyof 的一个典型用途是限制访问对象的 key 合法化,因为 any 做索引是不被接受的。
function getValue (p: Person, k: keyof Person) {return p[k]; // 如果k不如此定义,则无法以p[k]的代码格式通过编译}
实例类型获取typeof
typeof 关键词除了做类型保护,还可以从实现推导出类型,语法格式类型 = typeof 实例对象。
注意:此时的
typeof是一个类型关键词,只可以用在类型语法中。
function fn(x: string) {return x.length}const obj = {x: 1,y: '2'}type T0 = typeof fn // (x: string) => numbertype T1 = typeof obj // {x: number; y: string }
typeof 可以和 keyof 一起使用(因为 typeof 是返回一个类型嘛),如下:
const me = { name: 'gzx', age: 16 }type PersonKey = keyof typeof me; // 'name' | 'age'
遍历属性in
in也是一个类型关键词, 可以对联合类型进行遍历,只可以用在 type 关键词下面。语法格式[ 自定义变量名 in 枚举类型 ]: 类型
type Person = {[key in 'name' | 'age']: number}// { name: number; age: number; }
[ ] 操作符
使用 [] 操作符可以进行索引访问,也是一个 类型关键词
interface Person {name: stringage: number}type x = Person['name'] // x is string
综合应用例
写一个类型复制的类型工具:
type Copy<T> = {[key in keyof T]: T[key]}interface Person {name: stringage: number}type Person1 = Copy<Person>
类型推断
ts 中的类型推断是非常强大,而且其内部实现也是非常复杂的。
基本类型推断:
// ts 推导出 x 是 number 类型let x = 10
对象类型推断:
// ts 推断出 myObj 的类型:myObj: { x: number; y: string; z: boolean; }const myObj = {x: 1,y: '2',z: true}
函数类型推断:
// ts 推导出函数返回值是 number 类型function len (str: string) {return str.length}
上下文类型推断:
// ts 推导出 event 是 ProgressEvent 类型const xhr = new XMLHttpRequest()xhr.onload = function (event) {}
所以有时候对于一些简单的类型可以不用手动声明其类型,让 ts 自己去推断。
类型兼容性
typescript 的子类型是基于 结构子类型 的,只要结构可以兼容,就是子类型。(Duck Type)
class Point {x: number}function getPointX(point: Point) {return point.x}class Point2 {x: number}let point2 = new Point2()getPointX(point2) // OK
java、c++ 等传统静态类型语言是基于 名义子类型 的,必须显示声明子类型关系(继承),才可以兼容。
public class Main {public static void main (String[] args) {getPointX(new Point()); // okgetPointX(new ChildPoint()); // okgetPointX(new Point1()); // error}public static void getPointX (Point point) {System.out.println(point.x);}static class Point {public int x = 1;}static class Point2 {public int x = 2;}static class ChildPoint extends Point {public int x = 3;}}
对象子类型
子类型中必须包含源类型所有的属性和方法:
function getPointX(point: { x: number }) {return point.x}const point = {x: 1,y: '2'}getPointX(point) // OK
注意: 如果直接传入一个对象字面量是会报错的:
function getPointX(point: { x: number }) {return point.x}getPointX({ x: 1, y: '2' }) // error
这是 ts 中的另一个特性,叫做: excess property check ,当传入的参数是一个对象字面量时,会进行额外属性检查。
函数子类型
介绍函数子类型前先介绍一下逆变与协变的概念,逆变与协变并不是 TS 中独有的概念,在其他静态语言中也有相关理念。
在介绍之前,先假设一个问题,约定如下标记:
A ≼ B表示 A 是 B 的子类型,A 包含 B 的所有属性和方法。A => B表示以 A 为参数,B 为返回值的方法。(param: A) => B
如果我们现在有三个类型 Animal 、 Dog 、 WangCai(旺财) ,那么肯定存在下面的关系:
WangCai ≼ Dog ≼ Animal // 即旺财属于狗属于动物
问题:以下哪种类型是 Dog => Dog 的子类呢?
WangCai => WangCaiWangCai => AnimalAnimal => AnimalAnimal => WangCai
从代码来看解答:
class Animal {sleep: Function}class Dog extends Animal {// 吠bark: Function}class WangCai extends Dog {dance: Function}function getDogName (cb: (dog: Dog) => Dog) {const dog = cb(new Dog())dog.bark()}// 对于入参来说,WangCai 是 Dog 的子类,Dog 类上没有 dance 方法, 产生异常。// 对于出参来说,WangCai 类继承了 Dog 类,肯定会有 bark 方法getDogName((wangcai: WangCai) => {wangcai.dance()return new WangCai()})// 对于入参来说,WangCai 是 Dog 的子类,Dog 类上没有 dance 方法, 产生异常。// 对于出参来说,Animal 类上没有 bark 方法, 产生异常。getDogName((wangcai: WangCai) => {wangcai.dance()return new Animal()})// 对于入参来说,Animal 类是 Dog 的父类,Dog 类肯定有 sleep 方法。// 对于出参来说,WangCai 类继承了 Dog 类,肯定会有 bark 方法getDogName((animal: Animal) => {animal.sleep()return new WangCai()})// 对于入参来说,Animal 类是 Dog 的父类,Dog 类肯定有 sleep 方法。// 对于出参来说,Animal 类上没有 bark 方法, 产生异常。getDogName((animal: Animal) => {animal.sleep()return new Animal()})
可以看到只有 Animal => WangCai 才是 Dog => Dog 的子类型,可以得到一个结论,对于函数类型来说,函数参数的类型兼容是反向的,我们称之为 逆变 (个人理解: 即传入的参数是函数类型定义参数的子集,可以不用全传),返回值的类型兼容是正向的,称之为 协变 (个人理解: 返回的类型必须是包含全部的函数类型,不能少)。
逆变与协变的例子只说明了函数参数只有一个时的情况,如果函数参数有多个时该如何区分?
其实函数的参数可以转化为 Tuple 的类型兼容性:
type Tuple1 = [string, number]type Tuple2 = [string, number, boolean]let tuple1: Tuple1 = ['1', 1]let tuple2: Tuple2 = ['1', 1, true]let t1: Tuple1 = tuple2 // oklet t2: Tuple2 = tuple1 // error
可以看到 Tuple2 => Tuple1 ,即长度大的是长度小的子类型,再由于函数参数的逆变特性,所以函数参数少的可以赋值给参数多的(参数从前往后需一一对应),从数组的 forEach 方法就可以看出来:
[1, 2].forEach((item, index) => {console.log(item)}) // ok[1, 2].forEach((item, index, arr, other) => {console.log(other)}) // error
高级类型
联合类型与交叉类型
联合类型 (union type) 表示多种类型的 “或” 关系
function genLen(x: string | any[]) {return x.length}genLen('') // okgenLen([]) // okgenLen(1) // error
交叉类型表示多种类型的 “与” 关系
interface Person {name: stringage: number}interface Animal {name: stringcolor: string}const x: Person & Animal = {name: 'x',age: 1,color: 'red}
使用联合类型表示枚举:可以避免使用 enum 侵入了运行时。
type Position = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'const position: Position = 'UP'
类型保护
ts 初学者很容易写出下面的代码:
function isString (value) {return Object.prototype.toString.call(value) === '[object String]'}function fn (x: string | number) {if (isString(x)) {return x.length // error 类型“string | number”上不存在属性“length”。} else {// .....}}
如何让 ts 推断出来上下文的类型呢?
1. 使用 ts 的 **is** 关键词
function isString (value: unknown): value is string {return Object.prototype.toString.call(value) === '[object String]'}function fn (x: string | number) {if (isString(x)) {return x.length} else {// .....}}
2. typeof 关键词
在 ts 中,代码实现中的 typeof 关键词能够帮助 ts 判断出变量的基本类型:
function fn (x: string | number) {if (typeof x === 'string') { // x is stringreturn x.length} else { // x is number// .....}}
function fn2 (x?: string) {return x!.length}
3. instanceof 关键词
在 ts 中,instanceof 关键词能够帮助 ts 判断出构造函数的类型:
function fn1 (x: XMLHttpRequest | string) {if (x instanceof XMLHttpRequest) { // x is XMLHttpRequestreturn x.getAllResponseHeaders()} else { // x is stringreturn x.length}}
4. 针对 null 和 undefined 的类型保护
在条件判断中,ts 会自动对 null 和 undefined 进行类型保护:
function fn2 (x?: string) {if (x) {return x.length}}
5. 针对 null 和 undefined 的类型断言
如果我们已经知道的参数不为空,可以使用 ! 来手动标记:
function fn2 (x?: string) {return x!.length}
泛型
泛型相当于一个类型的参数,在 ts 中,泛型可以用在 类、接口、方法、类型别名 等实体中。
小试牛刀:
// 普通类型定义type Dog<T> = { name: string, age: T }// 普通类型使用const dog: Dog<number> = { name: 'ww', age: 2 }// 类定义class Cat<T> {private type: T;constructor(type: T) { this.type = type; }}// 类使用const cat: Cat<number> = new Cat<number>(20); // 或简写 const cat = new Cat(20)// 函数定义function swipe<T, U>(value: [T, U]): [U, T] {return [value[1], value[0]];}// 函数使用swipe<Cat<number>, Dog<number>>([cat, dog]) // 或简写 swipe([cat, dog])
泛型推导与默认值
注意,如果对一个类型名定义了泛型,那么使用此类型名的时候一定要把泛型类型也写上去。 而对于变量来说,它的类型可以在调用时推断出来的话,就可以省略泛型书写。
在函数调用的场合,TS会自动根据变量定义时的类型推导出变量类型,所以可以省略对泛型类型定义的书写:
type Dog<T> = { name: string, type: T }function adopt<T>(dog: Dog<T>) { return dog };const dog = { name: 'ww', type: 'hsq' }; // 这里按照Dog类型的定义一个type为string的对象adopt(dog); // Pass: 函数会根据入参类型推断出type为string
若不适用函数泛型推导,若需要定义变量类型则必须指定泛型类型。
const dog: Dog<string> = { name: 'ww', type: 'hsq' } // 不可省略<string>这部分
如果不指定,可以使用泛型默认值的方案。
type Dog<T = any> = { name: string, type: T }const dog: Dog = { name: 'ww', type: 'hsq' }dog.type = 123; // 不过这样type类型就是any了,无法自动推导出来,失去了泛型的意义
泛型约束extends
可以使用 extends 关键词来约束泛型的范围和形状。
type Lengthwise = {length: number}function createList<T extends number | Lengthwise>(): T[] {return [] as T[]}const numberList = createList<number>() // okconst stringList = createList<string>() // ok string 类型也是有 length 属性的const arrayList = createList<any[]>() // ok any[] 是一个数组类型,数组类型是有 length 属性的const boolList = createList<boolean>() // error
泛型约束也可以用在多个泛型参数的情况:
// 限制了 U 一定是 T 的 key 类型中的子集,这种用法常常出现在一些泛型工具库中。function pick<T, U extends keyof T>(){};
泛型条件extends
extends 除了做约束类型,还可以做条件控制,相当于与一个三元运算符,只不过是针对 类型 的。
表达式:T extends U ? X : Y
含义:如果 T 是 U 的子类型,则返回 X类型,否则返回 Y类型。,例如:
type IsNumber<T> = T extends number ? true : falsetype x = IsNumber<string> // false
对于 T extends U ? X : Y 来说,还存在一个特性,当 T 是一个联合类型时,会进行条件分发。
type Union = string | numbertype isNumber<T> = T extends number ? 'isNumber' : 'notNumber'type UnionType = isNumber<Union> // 'notNumber' | 'isNumber'
实际上,extends 运算会变成如下形式:
(string extends number ? 'isNumber' : 'notNumber') | (number extends number ? 'isNumber' : 'notNumber')
泛型推断infer
infer 关键字一般是搭配上面的泛型条件语句使用的,所谓推断,就是你不用预先指定在泛型列表中,在运行时会自动判断,不过得先预定义好整体的结构。举个例子:
type Foo<T> = T extends {t: infer Test} ? Test: string// 首选看 extends 后面的内容,{t: infer Test}可以看成是一个包含t属性的类型定义,这个t属性的 value 类型通过infer进行推断后会赋值给Test类型,如果泛型实际参数符合{t: infer Test}的定义那么返回的就是Test类型,否则默认给缺省的string类型。举个例子加深下理解:type One = Foo<number> // string,因为number不是一个包含t的对象类型type Two = Foo<{t: boolean}> // boolean,因为泛型参数匹配上了,使用了infer对应的typetype Three = Foo<{a: number, t: () => void}> // () => void,泛型定义是参数的子集,同样适配
infer用来对满足的泛型类型进行子类型的抽取,有很多高级的泛型工具也巧妙的使用了这个方法。内置的ReturnType 就是基于此特性实现的:
type ReturnType<T> = T extends (...args: any) => infer R ? R : nevertype Fn = (str: string) => numbertype FnReturn = ReturnType<Fn> // number
映射类型
映射类型相当于一个类型的函数,可以做一些类型运算,输入一个类型,输出另一个类型,前文我们举了个 Copy 的例子。
Partial<T>— 将T中的所有属性变成可选。Record<K, T>— 将k中所有的属性值转换为T类型。Readonly<T>— 将T中的所有属性变成只读。Pick<T, U>— 选择T中可以赋值给U的类型。(T必须全部包含U)Omit<T, U>— 适用于键值对对象的 Exclude,去除类型T中包含U的键值对。与Pick结果完全相反,Omit是取非结果Exclude<T, U>— 从T中剔除可以赋值给U的类型。相当于取差集Extract<T, U>— 提取T中可以赋值给U的类型。相当于取交集(U不必全部包含T)ReturnType<T>— 获取函数返回值类型。Required—将类型 T 中所有的属性变为必选项。NonNullable<T>— 从T中剔除null和undefined。InstanceType<T>— 获取构造函数类型的实例类型。 ```typescript type Animal = { name: string, category: string, age: number, eat: () => number }
// Partial的实现: 将每一个属性都变成可选
type Partial
// Record的实现: 将 K 中所有属性值转化为 T 类型,我们常用它来申明一个普通 object 对象。
type Record
}
// Record的使用
const obj: Record
// Readonly的实现: 将每一个属性都变成只读
type Readonly
// Pick的实现: 选择对象中的某些属性,生成新的子键值对类型
type Pick
}
// Pick的使用,用上面的Animal定义
const bird: Pick
// Omit的实现: 先去除 T 与 K 重叠的key,接着使用Pick把T类型和剩余的key组合起来即可。
type Omit = Pick
// Exclude的实现: 去除 T 类型和 U 类型的交集,返回无交集的剩余部分。即取差集
type Exclude
// Extract的实现: 基于extends特性,再配合 never 幺元的特性实现,取交集
type Exclude
// ReturnType的实现
type ReturnType
// Required的实现
type Required
·······
<a name="oL1Jd"></a># 模块<a name="AB24x"></a>## 全局模块 vs. 文件模块默认情况下,我们所写的代码是位于全局模块下的:```typescriptconst foo = 2
此时,如果我们创建了另一个文件,并写下如下代码,ts 认为是正常的:
const bar = foo // ok
如果要打破这种限制,只要文件中有 import 或者 export 表达式即可:
export const bar = foo // error
模块解析策略
Tpescript 有两种模块的解析策略:Node 和 Classic。当 tsconfig.json 中 module 设置成 AMD、System、ES2015 时,默认为 classic ,否则为 Node ,也可以使用 moduleResolution 手动指定模块解析策略。
两种模块解析策略的区别在于,对于下面模块引入来说:
import moduleB from 'moduleB'
Classic 模式的路径寻址:
/root/src/folder/moduleB.ts/root/src/folder/moduleB.d.ts/root/src/moduleB.ts/root/src/moduleB.d.ts/root/moduleB.ts/root/moduleB.d.ts/moduleB.ts/moduleB.d.ts
Node 模式的路径寻址:
/root/src/node_modules/moduleB.ts/root/src/node_modules/moduleB.tsx/root/src/node_modules/moduleB.d.ts/root/src/node_modules/moduleB/package.json (如果指定了"types"属性)/root/src/node_modules/moduleB/index.ts/root/src/node_modules/moduleB/index.tsx/root/src/node_modules/moduleB/index.d.ts/root/node_modules/moduleB.ts/root/node_modules/moduleB.tsx/root/node_modules/moduleB.d.ts/root/node_modules/moduleB/package.json (如果指定了"types"属性)/root/node_modules/moduleB/index.ts/root/node_modules/moduleB/index.tsx/root/node_modules/moduleB/index.d.ts/node_modules/moduleB.ts/node_modules/moduleB.tsx/node_modules/moduleB.d.ts/node_modules/moduleB/package.json (如果指定了"types"属性)/node_modules/moduleB/index.ts/node_modules/moduleB/index.tsx/node_modules/moduleB/index.d.ts
声明文件
什么是声明文件
声明文件已 .d.ts 结尾,用来描述代码结构,一般用来为 js 库提供类型定义。
平时开发的时候有没有这种经历:当用 npm 安装了某些包并使用的时候,会出现这个包的语法提示,下面是 vue 的提示:
这个语法提示就是声明文件的功劳了,先来看一个简单的声明文件长啥样,这是jsonp这个库的声明文件:
type CancelFn = () => void;type RequestCallback = (error: Error | null, data: any) => void;interface Options {param?: string;prefix?: string;name?: string;timeout?: number;}declare function jsonp(url: string, options?: Options, cb?: RequestCallback): CancelFn;declare function jsonp(url: string, callback?: RequestCallback): CancelFn;export = jsonp;
有了这份声明文件,编辑器在使用这个库的时候就可以根据这份声明文件来做出相应的语法提示。
编辑器是怎么找到这个声明文件?
- 如果这个包的根目录下有一个
index.d.ts,那么这就是这个库的声明文件了。 - 如果这个包的
package.json中有types或者typings字段,那个该字段指向的就是这个包的声明文件。
上述两种都是将声明文件写在包里面的情况,如果某个库很长时间不维护了,或者作者消失了该怎么办,没关系,typescript官方提供了一个声明文件仓库,尝试使用@types前缀来安装某个库的声明文件:**npm i @types/lodash**,当引入lodash的时候,编辑器也会尝试查找node_modules/@types/lodash 来为你提供lodash的语法提示。
还有一种就是自己写声明文件,编辑器会收集项目本地的声明文件,如果某个包没有声明文件,你又想要语法提示,就可以自己在本地写个声明文件:
// types/lodash.d.tsdeclare module "lodash" {export function chunk(array: any[], size?: number): any[];export function get(source: any, path: string, defaultValue?: any): any;}

如果源代码是用ts写的,在编译成js的时候,只要加上-d 参数,就能生成对应的声明文件。
tsc -d
注意,如果某个库有声明文件了,编辑器就不会再关心这个库具体的代码了,它只会根据声明文件来做提示。
window['myprop'] = 1 // OKwindow.myprop // 类型“Window & typeof globalThis”上不存在属性“myprop”window['myprop'] // ok,但是没有提示,没有类型
扩展原生对象
可能写过 ts 的小伙伴有这样的疑惑,我该如何在 window 对象上自定义属性呢?
window.myprop = 1 // error
默认的,window 上是不存在 myprop 这个属性的,所以不可以直接赋值,当然,可以使用方括号赋值语句,但是 get 操作时也必须用 [] ,并且没有类型提示。
window['myprop'] = 1 // OKwindow.myprop // 类型“Window & typeof globalThis”上不存在属性“myprop”window['myprop'] // ok,但是没有提示,没有类型
此时可以使用声明文件扩展其他对象,在项目中随便建一个xxx.d.ts:
// index.d.tsinterface Window {myprop: number}// index.tswindow.myprop = 2 // ok
也可以在模块内部扩展全局对象:
import A from 'moduleA'window.myprop = 2declare global {interface Window {myprop: number}}
扩展其他模块
如果使用过 ts 写过 vue 的同学,一定都碰到过这个问题,如何扩展 vue.prototype 上的属性或者方法?
import Vue from 'vue'Vue.prototype.myprops = 1const vm = new Vue({el: '#app'})// 类型“CombinedVueInstance<Vue, object, object, object, Record<never, any>>”// 上不存在属性“myprops”console.log(vm.myprops)
Vue 给出的方案,在项目中的 xxx.d.ts 中扩展 vue 实例上的属性:
import Vue from 'vue'declare module 'vue/types/vue' {interface Vue {myprop: number}}
ts 提供了 declare module 'xxx' 的语法来扩展其他模块,这非常有利于一些插件化的库和包,例如 vue-router 扩展 vue 。
// vue-router/types/vue.d.tsimport Vue from 'vue'import VueRouter, { Route, RawLocation, NavigationGuard } from './index'declare module 'vue/types/vue' {interface Vue {$router: VueRouter$route: Route}}declare module 'vue/types/options' {interface ComponentOptions<V extends Vue> {router?: VueRouterbeforeRouteEnter?: NavigationGuard<V>beforeRouteLeave?: NavigationGuard<V>beforeRouteUpdate?: NavigationGuard<V>}}
如何处理非 js 文件
处理 **vue** 文件
对于所有以 .vue 结尾的文件,可以默认导出 Vue 类型,这是符合 vue单文件组件 的规则的。
declare module '*.vue' {import Vue from 'vue'export default Vue}
处理 css in js
对于所有的 .css,可以默认导出一个 any 类型的值,这样可以解决报错问题,但是丢失了类型检查。
declare module '*.css' {const content: anyexport default content}
import * as React from 'react'import * as styles from './index.css'const Error = () => (<div className={styles.centered}><div className={styles.emoji}>????</div><p className={styles.title}>Ooooops!</p><p>This page doesn't exist anymore.</p></div>)export default Error
其实不管是全局扩展还是模块扩展,其实都是基于 TS 声明合并 的特性,简单来说,TS 会将它收集到的一些同名的接口、类、类型别名按照一定的规则进行合并。
编译
ts 内置了一个 compiler (tsc),可以让我们把 ts 文件编译成 js 文件,配合众多的编译选项,有时候不需要 babel 我们就可以完成大多数工作。
常用的编译选项
tsc 在编译 ts 代码的时候,会根据 tsconfig.json 配置文件的选项采取不同的编译策略。下面是三个常用的配置项:
- target - 生成的代码的 JS 语言的版本,比如 ES3、ES5、ES2015 等。
- module - 生成的代码所需要支持的模块系统,比如 es2015、commonjs、umd 等。
- lib - 告诉 TS 目标环境中有哪些特性,比如 WebWorker、ES2015、DOM 等。
和 babel 一样,ts 在编译的时候只会转化新 语法,不会转化新的 API, 所以有些场景下需要自行处理 polyfill 的问题。
更改编译后的目录
tsconfig 中的 outDir 字段可以配置编译后的文件目录,有利于 dist 的统一管理。
{"compilerOptions": {"module": "umd","outDir": "./dist"}}
编译后的目录结构:
myproject├── dist│ ├── index.js│ └── lib│ └── moduleA.js├── index.ts├── lib│ └── moduleA.ts└── tsconfig.json
编译后输出到一个 js 文件中
对于 amd 和 system 模块,可以配置 tsconfig.json 中的 outFile 字段,输出为一个 js 文件。
如果需要输出成其他模块,例如 umd ,又希望打包成一个单独的文件,需要怎么做?
可以使用 rollup 或者 webpack :
// rollup.config.jsconst typescript = require('rollup-plugin-typescript2')module.exports = {input: './index.ts',output: {name: 'MyBundle',file: './dist/bundle.js',format: 'umd'},plugins: [typescript()]}
一些常用的 ts 周边库
- @typescript-eslint/eslint-plugin、@typescript-eslint/parser - lint 套件
- DefinitelyTyped -
@types仓库 - ts-loader、rollup-plugin-typescript2 - rollup、webpack 插件
- typedoc - ts 项目自动生成 API 文档
- typeorm - 一个 ts 支持度非常高的、易用的数据库 orm 库
- nest.js、egg.js - 支持 ts 的服务端框架
- ts-node - node 端直接运行 ts 文件
- utility-types - 一些实用的 ts 类型工具
- type-coverage - 静态类型覆盖率检测
一个提高开发效率的小技巧
大家在日常开发的时候,可能会经常用到 webpack 的路径别名,比如: import xxx from '@/path/to/name',如果编辑器不做任何配置的话,这样写会很尴尬,编译器不会给你任何路径提示,更不会给你语法提示。这里有个小技巧,基于 tsconfig.json 的 baseUrl和paths这两个字段,配置好这两个字段后,.ts文件里不但有了路径提示,还会跟踪到该路径进行语法提示。
可以把 tsconfig.json 重命名成jsconfig.json,.js文件里也能享受到路径别名提示和语法提示了。
Q: 偏好使用 interface 还是 type 来定义类型?
A: 从用法上来说两者本质上没有区别,但是从扩展的角度来说,type 比 interface 更方便拓展一些,想要做类型的扩展的话,type 只需要一个&,而 interface 要多写不少代码。
type Name = { name: string };interface IName { name: string };// 扩展type Person = Name & { age: number };interface IPerson extends IName {age: number};
另外 type 有一些 interface 做不到的事情,比如使用 | 进行枚举类型的组合,使用typeof获取定义的类型等等。
不过 interface 有一个比较强大的地方就是可以重复定义添加属性,比如我们需要给window对象添加一个自定义的属性或者方法,那么我们直接基于其 interface 新增属性就可以了。
declare global {interface Window { MyNamespace: any; }}
