注:本文大量引用了其他文章的精华之处,如果有冒犯还请见谅。
TS 诞生背景
JavaScript 的设计,只用了十天,且是为了应付公司的任务(设计师本人并没有兴趣)。初衷是为了实现一些简单的网页交互,并没有考虑设计为些大型应用的语言,因此存在很多不合理的设计。而 js 发展速度又很快,过早的标准化导致 js 的很多不合理设计被保留且不可能被更正。
语言类型按照【类型检查的时机】可以分为动态类型和静态类型,静态类型是在编译时就检查类型,动态类型则是在运行时确定类型;按照【是否允许隐式类型转换】分为强类型和弱类型,强类型一旦声明类型便不可变更,弱类型则存在隐式类型转换。
JS 被设计为动态弱类型语言,其变量类型是在运行时动态确定的(动态类型),并且可以随时改变(弱类型)。正因如此,JS 巨量错误都只能在运行时发现,在开发时容易写出难以觉察的错误,维护时又常发现难以理解的代码,极其不利于开发维护大型复杂应用。
为了解决 js 的类型安全和可维护性问题,业界做了很多尝试。最终,微软内部孵化并开源的 TypeScript 脱颖而出,并迅速统一了 js 的类型标准。
TS 对 JS 的类型增强
- 类型系统按照「类型检查的时机」来分类,可以分为动态类型和静态类型。js 是动态类型:
js
let foo = 1;
foo.split(' ');
上述代码在 js 中,只有运行时才会报错。但在 ts 中,编译阶段就会报错。因为 ts 中变量 foo 在声明时就被自动推断为 number 类型,而 number 类型上面并没有 split 方法。
- 类型系统按照「是否允许隐式类型转换」来分类,可以分为强类型和弱类型。js 是弱类型:
js
function sum(a, b){
return a + b
}
sum(100, 100) // 200
sum(100, '100') // '100100'
上述代码在 js 中运行并不会报错,但是运算明显不符合我们的预期。在 js 中,函数 sum 的入参可以是任意类型,而操作费 + 号的作用也可以有多种,既可以用于数组的相加,还可以用于字符串的拼接,还可以用于字符串的强制类型转换(真灵活啊)。因此不同的如此会产生不同的结果(不符合预期)。
ts
function sum(a: number, b: number){
return a + b
}
sum(100, 100) // 200
sum(100, '100') // Argument of type 'string' is not assignable to parameter of type 'number'.
在ts中给函数 sum 入参增加类型后,在编译阶段便可发现语法错误。
可以发现 ts 弥补了 js 的类型检查和类型安全问题,可以极大提高开发过程中的编码正确率。
TS 对 JS 的编辑器增强
得益于 ts 的静态类型检查,使其可以增强编辑器(IDE)的功能,提供代码补全、接口提示、跳转到定义、代码重构更牢靠等能力。
代码提示:
接口提示:
TS 对 JS 的语法增强
一个语法进入到 Stage 3 阶段后,TypeScript 就会实现它。而不用等待漫长的正式标准出台。
class:
ts
class Animal {
public name;
private age = 17;
protected constructor(name) {
this.name = name;
}
}
class Cat extends Animal {
constructor(name) {
super(name);
this.age // 编辑器提示:age是父类私有属性,无法访问
}
}
let a = new Animal('Jack');
// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.
public
private
protected
为 ts 中类的语法,编译成 js 后并不存在,且访问也并不受限。但在 ts 编码过程中需要遵循 ts 语法。
装饰器:
ts
function sealed(constructor: Function) {
Object.seal(constructor); // 不能添加新属性、不能删除现有属性等
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
上例装饰器 sealed 将 class 的构造函数和原型封闭起来。
TS 基础
TS 数据类型
- 基本类型
ts
let a: string = 'asdf'
let b: number = 1
let c: boolean = true
let d: void = undefined
let d: () => void = () => {}
let e: undefined = undefined
let f: null = null
基本类型可以完全自动推导,所以其实不需要额外写类型。
- 字符串字面量类型
ts
let str: 'small' | 'big' | 'fly'
str = 'small'
str = 'big'
str = 'fly'
- 任意类型
ts
let a: any // 任意类型
a = 1
a = 'asdf'
a = {}
可以看到变量被赋予 any 类型之后,就失去了使用 ts 的意义,重新回归弱类型了。
在日常编码中,要尽量避免使用 any 。
- never 类型
不存在的类型,多用于条件判断。
ts
type Extract<T, U> = T extends U ? T : never
由于 never 是三元运算的假值时的结果,导致该类型只返回判断条件为真的结果。
- 类型断言
将类型断言为另一个类型
ts
let a = {} as { a: string; b: number }
a = {
a: 'asdf',
b: 1,
}
- 联合类型
表示值可以是多种类型中的一个
ts
// 联合类型
let a: string | number = 1
a = 'asdf'
a.toString() // 联合类型只能使用其公共属性/方法
a = 1
;(a as unknown as string).length // 或者使用断言
// 不建议使用接口的联合类型
let b: {a: string} | {b: number}
b = {a: 'asdf'}
b = {b: 1}
b.a // 报错
b.b // 报错
- 交叉类型
交叉类型即同时满足多个类型约束
ts
// 交叉类型不可用于基本类型
let a: string & number // a 的实际类型为 never,因为 a 不可能即使 string 又是 number
let b: {a: number} & {b: string} = { a: 1, b: 'asdf' } // b 的实际类型为 {a: number; b: string}
- 数组类型
数组类型有两种书写方式
ts
let a: string[] = ['1', '2', '3', ...]
let b: Array<number> = [1, 2, 3, ...]
// 数组泛型可以传入联合类型、接口等
let c: Array<string | number> = [1, 'a', 2, 'b', 'c', ...]
- 元组
元组可以理解为更精确的数组
ts
let a: [string, number] = ['a', 1]
a[0].length
a[1].toFixed()
- 函数
ts
let fn = () => {} // 自动推导 () => void
let add2 = (a: number) => a + 2 // (a: number) => number
- 对象的类型------接口
ts
interface IResponse {
code: number
data: any
msg: string
}
let res: IResponse = {
code: 200,
data: true,
msg: '成功',
}
- 泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
ts
interface IResponse<T> {
code: number
msg: string
data: T
}
let res: IResponse<boolean> = {
code: 200,
data: true,
msg: '成功',
}
- 枚举
ts
enum Status {
SUCCESS = 200,
FAIL = 0,
NOTFOUND = 404,
}
if (code === Status.SUCCESS) {...}
TS 关键词特性
typeof
获取js变量的ts类型
js 中的 typeof
获取的是变量的 js 类型
ts 中的 typeof
获取的是变量的 ts 类型
ts
let fn = () => {}
type Fn = typeof fn // 等同于 type Fn = () => void
keyof
索引查询和访问
keyof T
的结果为类型 T 上所有公有属性key的联合:
ts
interface Eg1 {
name: string,
readonly age: number,
}
type T1 = keyof Eg1 // T1的类型实则是name | age
type T2 = Eg1[keyof Eg1] // string | number
in
属性遍历
in
用来遍历联合类型,用于遍历对象类型的属性
ts
type P<T> = {
[K in keyof T]: T[K]
}
extends
继承条件判断
- 用于接口继承
ts
interface T1 {
name: string,
}
interface T2 {
sex: number,
}
interface T3 extends T1, T2 {
age: number,
}
// T3 = {name: string, sex: number, age: number}
- 用于三元运算中做条件判断
ts
type P1<T> = T extends string | number ? string : boolean
P<string> // string
P<number> // string
P<void> // boolean
若extends
前面的类型是泛型,且泛型传入的是联合类型时,则会依次判断该联合类型的所有子类型是否可分配给extends后面的类型(即可分裂性)。
ts
// extends前非泛型
type A2 = 'x' | 'y' extends 'x' ? 1 : 2; // A2结果为 2
// extends前为泛型,且传入是联合类型
type P<T> = T extends 'x' ? 1 : 2;
type A3 = P<'x' | 'y'> // A3结果为 1
infer
推断为
infer
用于extends
的条件类型中让Ts自己推到类型。
ts
type P<T extends Function> = T extends (...args: infer U) => any ? U : never
let a: P<(a: string, b: number) => void> // [a: string, b: number]
泛型 P 的参数是个函数,通过 infer 将该函数参数推断为 U,并返回此类型。
infer 只能用于条件句中的extends
之后、?
之前
类型兼容性
在类型系统中,判断是父类型还是子类型,只需要记住:父类型更宽泛。
下面我们基于类型的可赋值性、协变、逆变、双向协变等进行进一步的讲解。
- 可赋值性
父类型可以被(子类型)赋值。
在接口中:
ts
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
// 相对Dog,Animal更宽泛,是父类型。
let animal: Animal;
let dog: Dog;
animal = dog; // 可以赋值,子类型更佳具体,可以赋值给更佳宽泛的父类型
dog = animal; // 不可以赋值
在联合类型中:
ts
type A = 1 | 2 | 3;
type B = 2 | 3;
// 相对B,A更宽泛,是父类型。
let a: A;
let b: B;
b = a; // 不可赋值
a = b; // 可以赋值
- 协变
父子类型通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变。
ts
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
let Eg1: Animal;
let Eg2: Dog;
Eg1 = Eg2; // 兼容,可以赋值
let Eg3: Array<Animal>
let Eg4: Array<Dog>
Eg3 = Eg4 // 兼容,可以赋值
通过Eg3
和Eg4
来看,在Animal
和Dog
在变成数组后,Array<Dog>
依旧可以赋值给Array<Animal>
,这就是协变的。
- 逆变
父子类型通过某种构造关系构造成的新的类型,如果父子关系逆转了则是逆变。
ts
interface Animal {
name: string;
}
interface Dog extends Animal {
break(): void;
}
type AnimalFn = (arg: Animal) => void
type DogFn = (arg: Dog) => void
let animal: AnimalFn = (arg: Animal) => {}
let dog: DogFn = (arg: Dog) => { arg.break(); }
animal = dog; // 不可赋值
dog = animal; // 可以赋值
animal = dog
后其类型仍是 AnimalFn(假设不报错),在调用 animal({ name: 'cat' })
时就会缺少参数break,而 ts 类型系统并不会报错;
dog = animal
后其类型仍是 DogFn,在调用 dog({ name: 'cat', break(){} })
时多了参数break,这无所谓。
所以牢记函数参数处于逆变位置哦。
- infer 的协变逆变
infer
推导的名称相同并且都处于逆变 的位置,则推导的结果将会是交叉类型。
ts
type Bar<T> = T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
} ? U : never;
// type T1 = string
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;
// type T2 = never
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;
infer
推导的名称相同并且都处于协变 的位置,则推导的结果将会是联合类型。
ts
type Foo<T> = T extends {
a: infer U;
b: infer U;
} ? U : never;
// type T1 = string
type T1 = Foo<{ a: string; b: string }>;
// type T2 = string | number
type T2 = Foo<{ a: string; b: number }>;
TS 进阶
内置类型工具解析
Partial
Partial<T>
将T
的所有属性变成可选的。
ts
type Partial<T> = {
[K in keyof T]?: T[K]
}
[K in keyof T]
遍历所有属性?:
将属性变为可选T[K]
设置为原来的类型
扩展:如何实现指定字段变为可选,剩余不变?
ts
type PartialOption<T, U extends keyof T> = Omit<T, U> & Partial<Pick<T, U>>
Required
Required<T>
将T
的所有属性变成必选的。
ts
type Required<T> = {
[P in keyof T]-?: T[P]
}
-
用于移除特定修饰符的功能,如 ?
readonly
Readonly
Readonly<T>
将T
的所有属性变成只读的。
ts
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
Pick
挑选一组属性并组成一个新的类型。
ts
type Pick<T, P extends keyof T> = {
[K in P]: T[K]
}
Record
构造一个type
,key
为联合类型中的每个子类型,类型为T
。
ts
type Record2<U extends keyof any, T> = {
[K in U]: T
}
值得注意的是keyof any
得到的是string | number | symbol
Exclude
Exclude<T, U>
提取存在于T
,但不存在于U
的类型组成的联合类型。
ts
type Exclude<T, U> = T extends U ? never : T
Extract
Extract<T, U>
提取联合类型T
和联合类型U
的所有交集。
ts
type Extract<T, U> = T extends U ? T : never
Omit
Omit<T, K>
从类型T
中剔除K
中的所有属性。
ts
Parameters
Parameters 获取函数的参数类型,将每个参数类型放在一个元组中。
ts
type Parameters<T extends (...args: any) => any> = T extends (...args: infer U) => any ? U : never
ReturnType
ReturnType 获取函数的返回类型。
ts
type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never
Awaited
Awaited 获取 Promise 返回的类型
ts
type Awaited<T extends Promise<any>> = T extends Promise<infer U> ? U : never
NonNullable
从类型中排除 null 和 undefined 来构造一个新的 Type
ts
type NonNullable<T> = T extends null | undefined ? never : T
自定义类型工具
Prettier
在编辑器中,ts 总是优先展示接口名而不显示接口的实际内容:
这样我们就要时常要去接口的定义处看接口的实际内容到底是啥。为了能让接口直接显示出实际内容,可以自定义一个类型工具 Perttier :
ts
type Prettier<T> = T extends infer U ? {[K in keyof U]: U[K]} : never
ValueOf
ValueOf
与keyof
相对应。取出类型的所有 value 的联合类型
ts
type ValueOf<T extends Record<string, any>> = T[keyof T]
Mutable
用来将所有属性的readonly
移除
ts
type Mutable<T extends Record<string, any>> = {
-readonly [P in keyof T]: T[P]
}
ConvertNumberToString
将类型中的number
转换为string
类型
ts
type ConvertNumberToString<T extends Record<string, any>> = {
[K in keyof T]: T[K] extends number ? string : T[K]
}
SymmetricDifference
SymmetricDifference<T, U>
获取没有同时存在于T和U内的类型
ts
type SymmetricDifference<T, U> = Exclude<T | U, T & U>
FunctionKeys
获取T
中所有类型为函数的key
组成的联合类型
ts
type FunctionKeys<T extends Object> = {
[K in keyof T]: <NonNullable<T[K]>> extends Function ? K : never
}[keyof T]
OptionalKeys
OptionalKeys<T>
提取T中所有可选类型的key组成的联合类型
ts
type OptionalKeys<T> = {
[K in keyof T]: {} extends Pick<T, K> ? K : never
}[keyof T]
- 核心实现,用映射类型遍历所有key,通过
Pick<T, P>
提取当前key和类型。注意,这里也是利用了同态拷贝会拷贝可选修饰符的特性。 - 利用
{} extends {当前key: 类型}
判断是否是可选类型。
实战应用
使用泛型实现透传函数返回值
原先接口请求函数
ts
const request = (params: { url: string; data: any; method: string }) => {
return new Promise((resolve, reject) => {
uni.request({
...params,
async success(res: any) {
if (res.data.code === 200) {
resolve(res.data)
} else {
reject(res.data)
}
}
})
})
}
const getList = (): Promise<any> => {
return request(
{
url: 'https://...',
data: {},
method: 'GET',
}
)
}
// 使用
const res = await getList() // res是any
因为 request
方法返回值类型是 Promise<unknown>
, 所以只能给 getList
返回值定义 any
类型,导致使用时 ts 的类型推断为 any
失去了使用 ts 带来的便利。改造后:
ts
interface IResoponse<T> {
code: number
msg: string
data: T
}
const request = <T>(params: { url: string; data: any; method: string }): Promise<IResoponse<T>> => {
return new Promise((resolve, reject) => {
uni.request({
...params,
async success(res: {data: IResoponse<T>}) {
if (res.data.code === 200) {
resolve(res.data)
} else {
reject(res.data)
}
}
})
})
}
const getList = () => {
return request<string>(
{
url: 'https://...',
data: {},
method: 'GET',
}
)
}
// 使用
const res = await getList() // res是string
改造后 request
函数的返回值根据传入的泛型 T
确定:Promise<IResoponse<T>>
。 现在可以将函数的返回值类型定义前置,实现每个接口都有不同的返回值。
根据函数返回值格式不同自定义函数类型返回值工具
业务中有这么个点,接口请求的apigetList
,其返回值是 Promise<{ data: any; code: number }>
格式的。在业务中使用时为了偷懒不单独引入返回值类型,可以这样做:
ts
const list = ref<Awaited<ReturnType<typeof getList>>['data']>()
这样做没问题,但是看起来就很麻烦,毕竟那么多接口呢。所以进行封装:
ts
type RealReturnType<T extends Function> =
T extends (...args: any) => Promise<{ data: any; code: number }>
? Awaited<ReturnType<T>>['data']
: T extends (...args: any) => Promise<any>
? Awaited<ReturnType<T>>
: ReturnType<T>
首先限定入参 T 是函数类型,然后判断如果 T 继承了 (...args: any) => Promise<{ data: any; code: number }>
类型,则自动解构 Promise 并返回其 data。
其次如果不符合返回值格式为 { data: any; code: number }
的,则只解构 Promise 并返回。
最后如果不是 async 函数,则直接返回结果。可以看到主要是利用了 extends
关键字来判断函数类型,然后根据类型返回结果。
ts
// 需要封装的函数(接口)
const fn1 = async () => new Promise(r => r({ data: ['接口返回内容']; code: 200 }))
// 解构返回值
const list = ref<RealReturnType<typeof fn1>>()
// 需要封装的函数(可能是些常量的工厂函数)
const fn1 = async () => new Promise(r => r({a: 'a', b: 'b'}))
// 解构返回值
const list = ref<RealReturnType<typeof fn1>>()