在 2020 年的今天,TS 已经愈来愈火,不论是服务端(Node.js),仍是前端框架(Angular、Vue3),都有愈来愈多的项目使用 TS 开发,做为前端程序员,TS 已经成为一项必不可少的技能,本文旨在介绍 TS 中的一些高级技巧,提升你们对这门语言更深层次的认知。javascript
Typescript = Type + ECMAScript + Babel-Litecss
Typescript 设计目标: github.com/Microsoft/T…html
any
和 unknown
的区别any
: 任意类型unknown
: 未知的类型任何类型都能分配给 unknown
,但 unknown
不能分配给其余基本类型,而 any
啥都能分配和被分配。前端
let foo: unknown foo = true // ok foo = 123 //ok foo.toFixed(2) // error let foo1: string = foo // error 复制代码
let bar: any bar = true // ok bar = 123 //ok foo.toFixed(2) // ok let bar1:string = bar // ok 复制代码
能够看到,用了 any
就至关于彻底丢失了类型检查,因此你们尽可能少用 any
,对于未知类型能够用 unknown
。vue
咱们能够经过不一样的方式将 unknown
类型缩小为更具体的类型范围:java
function getLen(value: unknown): number { if (typeof value === 'string') { // 由于类型保护的缘由,此处 value 被判断为 string 类型 return value.length } return 0 } 复制代码
这个过程叫类型收窄(type narrowing)。node
never
通常表示哪些用户没法达到的类型。在最新的 typescript 3.7 中,下面代码会报错:react
// never 用户控制流分析 function neverReach (): never { throw new Error('an error') } const x = 2 neverReach() x.toFixed(2) // x is unreachable 复制代码
never
还能够用于联合类型的 幺元:webpack
type T0 = string | number | never // T0 is string | number 复制代码
function fn(): number { return 1 } const fn = function (): number { return 1 } const fn = (): number => { return 1 } const obj = { fn (): number { return 1 } } 复制代码
在
()
后面添加返回值类型便可。c++
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') // ok let b = toString(2) // ok let 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 | number const input = stringOrNumber(1) toString('hello') // ok toString(2) // ok toString(input) // error 复制代码
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()); // ok getPointX(new ChildPoint()); // ok getPointX(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 => WangCai
WangCai => Animal
Animal => Animal
Animal => 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 // ok let 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('') // ok genLen([]) // ok genLen(1) // error 复制代码
交叉类型表示多种类型的 “与” 关系
interface Person {
name: string
age: number
}
interface Animal {
name: string
color: string
}
const x: Person & Animal = {
name: 'x',
age: 1,
color: 'red
}
复制代码
使用联合类型表示枚举
type Position = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT' const position: Position = 'UP' 复制代码
能够避免使用
enum
侵入了运行时。
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 string return x.length } else { // x is number // ..... } } 复制代码
3. instanceof 关键词
在 ts 中,instanceof 关键词可以帮助 ts 判断出构造函数的类型:
function fn1 (x: XMLHttpRequest | string) { if (x instanceof XMLHttpRequest) { // x is XMLHttpRequest return x.getAllResponseHeaders() } else { // x is string return 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 } 复制代码
typeof
关键词除了作类型保护,还能够从实现推出类型,。
注意:此时的
typeof
是一个类型关键词,只能够用在类型语法中。
function fn(x: string) { return x.length } const obj = { x: 1, y: '2' } type T0 = typeof fn // (x: string) => number type T1 = typeof obj // {x: number; y: string } 复制代码
keyof
也是一个 类型关键词 ,能够用来取得一个对象接口的全部 key
值:
interface Person { name: string age: number } type PersonAttrs = keyof Person // 'name' | 'age' 复制代码
in
也是一个 类型关键词, 能够对联合类型进行遍历,只能够用在 type 关键词下面。
type Person = { [key in 'name' | 'age']: number } // { name: number; age: number; } 复制代码
使用 []
操做符能够进行索引访问,也是一个 类型关键词
interface Person { name: string age: number } type x = Person['name'] // x is string 复制代码
写一个类型复制的类型工具:
type Copy<T> = { [key in keyof T]: T[key] } interface Person { name: string age: number } type Person1 = Copy<Person> 复制代码
泛型至关于一个类型的参数,在 ts 中,泛型能够用在 类
、接口
、方法
、类型别名
等实体中。
function createList<T>(): T[] { return [] as T[] } const numberList = createList<number>() // number[] const stringList = createList<string>() // string[] 复制代码
有了泛型的支持,createList 方法能够传入一个类型,返回有类型的数组,而不是一个
any[]
。
若是咱们只但愿 createList 函数只能生成指定的类型数组,该如何作,可使用 extends
关键词来约束泛型的范围和形状。
type Lengthwise = { length: number } function createList<T extends number | Lengthwise>(): T[] { return [] as T[] } const numberList = createList<number>() // ok const stringList = createList<string>() // ok const arrayList = createList<any[]>() // ok const boolList = createList<boolean>() // error 复制代码
any[]
是一个数组类型,数组类型是有 length 属性的,因此 ok。string
类型也是有 length 属性的,因此 ok。可是boolean
就不能经过这个约束了。
extends
除了作约束类型,还能够作条件控制,至关于与一个三元运算符,只不过是针对 类型 的。
表达式:T extends U ? X : Y
含义:若是 T 能够被分配给 U,则返回 X,不然返回 Y。通常条件下,若是 T 是 U 的子类型,则认为 T 能够分配给 U,例如:
type IsNumber<T> = T extends number ? true : false type x = IsNumber<string> // false 复制代码
映射类型至关与一个类型的函数,能够作一些类型运算,输入一个类型,输出另外一个类型,前文咱们举了个 Copy
的例子。
// 每个属性都变成可选 type Partial<T> = { [P in keyof T]?: T[P] } // 每个属性都变成只读 type Readonly<T> = { readonly [P in keyof T]: T[P] } // 选择对象中的某些属性 type Pick<T, K extends keyof T> = { [P in K]: T[P]; } // ...... 复制代码
typescript 2.8 在 lib.d.ts
中内置了几个映射类型:
Partial<T>
-- 将 T
中的全部属性变成可选。Readonly<T>
-- 将 T
中的全部属性变成只读。Pick<T, U>
-- 选择 T
中能够赋值给U
的类型。Exclude<T, U>
-- 从T
中剔除能够赋值给U
的类型。Extract<T, U>
-- 提取T
中能够赋值给U
的类型。NonNullable<T>
-- 从T
中剔除null
和undefined
。ReturnType<T>
-- 获取函数返回值类型。InstanceType<T>
-- 获取构造函数类型的实例类型。因此咱们平时写 TS 时能够直接使用这些类型工具:
interface ApiRes { code: string; flag: string; message: string; data: object; success: boolean; error: boolean; } type IApiRes = Pick<ApiRes, 'code' | 'flag' | 'message' | 'data'> // { // code: string; // flag: string; // message: string; // data: object; // } 复制代码
对于 T extends U ? X : Y 来讲,还存在一个特性,当 T 是一个联合类型时,会进行条件分发。
type Union = string | number type isNumber<T> = T extends number ? 'isNumber' : 'notNumber' type UnionType = isNumber<Union> // 'notNumber' | 'isNumber' 复制代码
实际上,extends 运算会变成以下形式:
(string extends number ? 'isNumber' : 'notNumber') | (number extends number ? 'isNumber' : 'notNumber') 复制代码
Extract
就是基于此特性,再配合 never
幺元的特性实现的:
type Exclude<T, K> = T extends K ? never : T type T1 = Exclude<string | number | boolean, string | boolean> // number 复制代码
infer
关键词infer
能够对运算过程当中的类型进行存储,内置的ReturnType
就是基于此特性实现的:
type ReturnType<T> = T extends (...args: any) => infer R ? R : never type Fn = (str: string) => number type FnReturn = ReturnType<Fn> // number 复制代码
默认状况下,咱们所写的代码是位于全局模块下的:
const 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.ts declare 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 复制代码
声明文件该怎么写能够参考www.tslang.cn/docs/handbo…
还要注意的是,若是某个库有声明文件了,编辑器就不会再关心这个库具体的代码了,它只会根据声明文件来作提示。
可能写过 ts 的小伙伴有这样的疑惑,我该如何在 window 对象上自定义属性呢?
window.myprop = 1 // error 复制代码
默认的,window 上是不存在 myprop
这个属性的,因此不能够直接赋值,固然,能够输用方括号赋值语句,可是 get 操做时也必须用 []
,而且没有类型提示。
window['myprop'] = 1 // OK window.myprop // 类型“Window & typeof globalThis”上不存在属性“myprop” window['myprop'] // ok,可是没有提示,没有类型 复制代码
此时可使用声明文件扩展其余对象,在项目中随便建一个xxx.d.ts
:
// index.d.ts interface Window { myprop: number } // index.ts window.myprop = 2 // ok 复制代码
也能够在模块内部扩展全局对象:
import A from 'moduleA' window.myprop = 2 declare global { interface Window { myprop: number } } 复制代码
若是使用过 ts 写过 vue 的同窗,必定都碰到过这个问题,如何扩展 vue.prototype 上的属性或者方法?
import Vue from 'vue' Vue.prototype.myprops = 1 const 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.ts import 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?: VueRouter beforeRouteEnter?: NavigationGuard<V> beforeRouteLeave?: NavigationGuard<V> beforeRouteUpdate?: NavigationGuard<V> } } 复制代码
.vue
文件引入?处理 vue
文件
对于全部以 .vue
结尾的文件,能够默认导出 Vue 类型,这是符合 vue单文件组件
的规则的。
declare module '*.vue' { import Vue from 'vue' export default Vue } 复制代码
处理 css in js
对于全部的 .css
,能够默认导出一个 any 类型的值,这样能够解决报错问题,可是丢失了类型检查。
declare module '*.css' { const content: any export 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
配置文件的选项采起不一样的编译策略。下面是三个经常使用的配置项:
和 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
复制代码
对于 amd
和 system
模块,能够配置 tsconfig.json
中的 outFile
字段,输出为一个 js 文件。
若是须要输出成其余模块,例如 umd
,又但愿打包成一个单独的文件,须要怎么作?
可使用 rollup
或者 webpack
:
// rollup.config.js const typescript = require('rollup-plugin-typescript2') module.exports = { input: './index.ts', output: { name: 'MyBundle', file: './dist/bundle.js', format: 'umd' }, plugins: [ typescript() ] } 复制代码
@types
仓库你们在平常开发的时候,可能会常常用到webpack的路径别名,好比: import xxx from '@/path/to/name'
,若是编辑器不作任何配置的话,这样写会很尴尬,编译器不会给你任何路径提示,更不会给你语法提示。这里有个小技巧,基于 tsconfig.json
的 baseUrl
和paths
这两个字段,配置好这两个字段后,.ts
文件里不但有了路径提示,还会跟踪到该路径进行语法提示。
这里有个小彩蛋,能够把 tsconfig.json
重命名成jsconfig.json
,.js
文件里也能享受到路径别名提示和语法提示了。
使用 webstorm
的同窗若是也想使用的话,只要打开设置,搜索webpack,而后设置一下webpack配置文件的路径就行了。