作者:黄轩 openInula核心贡献者/架构SIG 成员
TypeScript的简介
TypeScript起源于JavaScript,它的设计初衷是微软为了解决JavaScript在大型项目开发中遇到的问题,TypeScript在JavaScript的基础上添加了一套图灵完备的类型系统,使得TypeScript具有良好的静态检查能力,可以胜任大型项目的编写。TypeScript是JavaScript的严格语法超集,因此任何现有的JavaScript程序都是合法的TypeScript程序,TypeScript可以通过tsc转换成JavaScript并生产对应的类型声明文件.d.ts
TypeScript
// add.ts
function add(a: number, b: number): number {
return a + b;
}
执行tsc add.ts --declaration
后生成js文件和对应的类型声明文件
JavaScript
// add.js
function add(a, b) {
return a + b;
}
Typescript
// add.d.ts
declare function add(a: number, b: number): number;
TypeScript基础知识
基础类型
布尔值 boolean
TypeScript
const status: boolean = ture
const hasRun: boolean = false
数字
TypeScript
let value: number = 123
let binary: number = 0b110
字符串
TypeScript
const name: string = 'xiaoming'
数组
TypeScript
const list: number[] = [1,3,5,7]
const list: Array<number> = [1,3,5,7]
元组
元组运行定义一个已知长度和元素类型的数组
TypeScript
const item: [string, number] = ['apple', 1]
枚举
TypeScript
enum Color {
Red,
Blue,
Green
}
Any
any代表任意类型,当不希望TypeScript去静态检查这个值的时候可以使用any类型
Unknown
unknown意味着变量类型未知,unknown类型在TypeScript3.0版本引入,相比于any,该类型的变量在使用前需要使用类型守卫收窄其类型,比any更安全
TypeScript
const s: unknown = '123'
s.lenght // error ts(18046)
if (typeof s === 'string'){
s.length
}
Void
当函数没有返回值时其返回类型为void
TypeScript
const handle: (content: string) => void = (content:string) => console.log(content)
Never
TypeScipt2.0引入了never类型。never是任何类型的子类型。用来表示当前不能返回值,和void的区别在于void表示的是返回为空,而never是永不返回
- 函数存在无法到达的终点会返回never
TypeScript
const err = (msg: string) => { throw new Error(msg) } //err: (msg: string) => never
- 一定失败的Promise
TypeScript
const p= Promise.reject('foo') //p: Promise<never>
TypeScript中的类型运算
交叉类型(&)
交叉类型类似 js 中的与运算符 &,将多种类型组合为一种类型
typescript
type LeftType = {
id: number;
left: string;
};
type RightType = {
id: number;
right: string;
};
type IntersectionType = LeftType & RightType
IntersectionType
具有id
,left
,right
三个属性
相同的类型可以合并,不同的类型无法合并,会被舍弃
typescript
type LeftType = {
id: string
left: string;
};
type RightType = {
id: number;
right: string;
};
type IntersectionType = LeftType & RightType
IntersectionType
具有id
,left
,right
三个属性,其中id
的类型为never
类型
联合类型(|)
联合类型类似 js 里的或运算符 |,表示其类型为多个类型中的任意一个
TypeScript
type UnionType = string | number;
let a: UnionType
变量a是
string
和number
的联合类型,因此a只有string
和number
共有的方法
同样的如果是两个类型的联合类型,也只能访问两个类型共有的属性
在使用联合类型时,通常要配合类型守卫缩窄对象的类型,确保类型安全
映射类型
对象、class 在 TypeScript 对应的类型是索引类型(Index Type)
keyof
是查询索引类型中的所有索引
TypeScript
interface A {
name:string
age: number
}
type K = keyof A // 'name'|'age'
T[key]
是取索引类型某个索引的值
TypeScript
type T = A['name'] // T string
in 是用于遍历联合类型的运算符
条件类型(extends)
TypeScript 里的条件判断是 extends ? :
,叫做条件类型(Conditional Type)
TypeScript
T extends U ? X : Y
如果T是U的子类型,返回X否则返回Y
TypeScript
type A = true extends boolean ? '1' : '2' // type A '1'
type B = boolean extends true ? '1' : '2' // type B '2'
使用条件类型结合泛型就能实现传入不同类型的参数,返回新的类型
TypeScript
type isZero<T> = T extends 0 ? true : false
type a=isZero<1> // a = false
type b=isZero<0> // b = true
类型推导(infer)
infer
是TypeScript2.8中新增的关键词,用于提前类型的一部分,infer的使用非常灵活,几乎所有复杂的类型都离不开infer
TypeScript
type First<Tuple extends unknown[]> = Tuple extends [infer T,...infer R] ? T : never;
type res = First<[1,'2',3]>; // res = 1
使用infer
配合泛型递归,可以实现更为复杂的功能
TypeScript
type ClearLeadingSlash<S extends string> = S extends `/${infer R}` ? ClearLeadingSlash<R> : S
type Res=ClearLeadingSlash<"/////123"> // type Res = '123'
配合泛型的默认值,infer
还可以实现字符串长度的统计
TypeScript
type Len<S extends string, Arr extends any[] = []> = S extends `${infer F}${infer L}` ? Len<L, [...Arr, F]> : Arr['length']
type R=Len<"hello world!"> // type R = 12
内置工具泛型
Partial
把索引类型中的所有属性变成可选的
TypeScript
type Partial<T> = {
[P in keyof T]?: T[P];
};
Required
把索引类型中的所有属性变成必选的
TypeScript
type Required<T> = {
[P in keyof T]-?: T[P];
};
Readonly
把索引类型中的所有属性变成只读的
TypeScript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Pick
从索引类型T中选择一些属性组成一个新的类型
TypeScript
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
TypeScript
type A = {
color: string
price: number
category: string
}
type B = Pick<A, 'color'|'price'> // type B = {color: string, price: number}
Exclude
TypeScript
type Exclude<T, U> = T extends U ? never : T;
从类型T中去掉类型U
TypeScript
type C = Exclude<'1'|'2'|'3','2'> // type C = '1'|'3'
Extract
typescript
type Extract<T, U> = T extends U ? T : never;
从类型T中提取类型U
typescript
type C = Extract<'a' | 'b' | 'c' | 'd', 'b' | 'c'> // type C = 'b'|'c'
Omit
从索引类型T中删除一些属性组成一个新的类型
TypeScript
type A = {
color: string
price: number
category: string
}
type B = Omit<A, 'color'|'price'> // type B = {category: string}
Record
构造具有给定类型T
的一组属性K
的类型
TypeScript
type Record<K extends keyof any, T> = {
[P in K]: T;
};
TypeScript
type A = Record<'id'|'name',string> // type A = {id: string, name: string}
Awaited
递归取出Promise
的返回类型
typescript
type P = Promise<Promise<Promise<number>>>
type R = Awaited<P> // type R = number
NonNullable
排除null和undefined类型
TypeScript
type NonNullable<T> = T & {};
TypeScript高级用法
类型守卫
类型守卫是用于判断变量类型的一系列语句,上面提到的unkonwn
类型或联合类型通常需要配合类型守卫使用收窄变量的类型后使用。
类型守卫通常有
in
typeof
instanceof
Array.isArray
- 自定义类型
typescript
interface Teacher {
name: string;
courses: string;
}
interface Student {
name: string;
study: string;
}
type Person = Teacher | Student;
function getInfo(val: Person) {
//此时val类型缩小为Teacher类型
if ('courses' in val) {
console.log(val.courses)
}
//此时val类型缩小为Student类型
if ('study' in val) {
console.log(val.study)
}
}
typeof
只能判断以下基本类型
- Boolean
- String
- Undefined
- Function
- Number
- Bigint
- Symbol
typescript
function getName(id: string|number) {
//推断id为number类型
if (typeof id === "number") {
return id.toString().toUpperCase();
}
//推断id为string类型
if (typeof id === "string") {
return id.toUpperCase();
}
}
instanceof (例子1中的interface换成class)
typescript
class Teacher {
name: string;
courses: string;
}
class Student {
name: string;
study: string;
}
type Person = Teacher | Student;
function getInfo(val: Person) {
//此时val类型缩小为Teacher类型
if ('courses' instanceof val) {
console.log(val.courses)
}
//此时val类型缩小为Student类型
if ('study' instanceof val) {
console.log(val.study)
}
}
Array.isArray
typescript
function getInfo(name: string|string[]): string[] {
if (Array.isArray(name)) {
// 此处name被推断为string[]
return name
}
return [name]
}```
自定义类型重载
```typescript
function isPopStateEvent(event: Event): event is PopStateEvent {
return event.type === 'popState'
}
function getState(event: Event) {
if (isPopStateEvent(event)) {
// event被推断为PopStateEvent
event.state
}
return {}
}
类型守卫通常在函数接受参数的类型范围较大时使用,配合上述方法收窄类型,写出类型更安全的代码
函数重载
在TypeScript的函数中,如果函数可以接受的参数数量类型不确定,或接受的参数类型与返回的结果类型有关系,那么就要用到TypeScript中的函数重载了
先看一个例子
TypeScript
function greet(person: string | string[]): string | string[] {
if (typeof person === 'string') {
return `Hello, ${person}!`;
} else if (Array.isArray(person)) {
return person.map(name => `Hello, ${name}!`);
}
throw new Error('Unable to greet');
}
在这个例子中参数接受两种类型,返回两种类型,使用联合类型的写法配合类型守卫可以保证函数的类型安全,但是这样的写法返回的也是一个联合类型,在使用函数返回值时还需要再次进行判断。
如果使用函数重载的方法实现
在输入不同的参数就能推导出正确的返回结果
typescript
function greet(person: string): string
function greet(person: string[]): string[]
其中这两行代码声明了两个重载签名,在类型推导时会使用类型能匹配的签名进行类型推导
typescript
function greet(person: unknown): unknown {
if (typeof person === 'string') {
return `Hello, ${person}!`;
} else if (Array.isArray(person)) {
return person.map(name => `Hello, ${name}!`);
}
throw new Error('Unable to greet');
}
这部分是函数的实现签名,实现签名中参数的类型必须是每个重载签名的超集。
断言
所有类型断言对运行时没有影响,只在静态类型检查中生效,因此不推荐大量使用断言来解决类型错误,会影响到代码的类型安全
类型断言
TypeScript允许用户覆盖编译器的类型推断
尖括号语法
typescript
let value: any = "this is a string";
let length: number = (<string>value).length;
尖括号语法不能在tsx
文件中使用,因此通常使用更通用的as
语法
as语法
typescript
let value: any = "this is a string";
let length: number = (value as string).length;
typescript4.9中引入了satisfies
关键词,其用法和as
相似,改进使用as
进行断言的一些问题,satisfies
会保留该表达式最具体的类型用于类型推断
typescript
interface Person {
id: string | number
name?: string
}
const a: Person = { id: '1', name: 'xiaoming' }
const b = { id: '2', name: 'xiaowang' } as Person
// a和b的id都会被推断为string|number, name被推断为string|undefined
// 要获取a.id的长度
(a.id as string).length
// 使用satisfied 最匹配的类型会被保留
const c = { id: '3', name: 'xiaogang' } satisfies Person
c.id.length
非空断言
当你明确知道某个值不可能为undefined
和null
时,你可以用 在变量后面加上一个!
来告诉编译器这个变量确定不会空
tsx
ReactDOM.createRoot(document.getElementById('root')!).render(<App/>)
确定赋值断言
TypeScript2.7中引入了确定赋值断言,在下面场景下
typescript
let value: number
init()
console.log(value) // err 在赋值前使用了变量"value"。ts(2454)
function init() {
value = 1
}
要解决这错误,要么将value
声明为number|undefined
,要么使用确定赋值断言,告诉编译器value一定会被赋值
typescript
let value!: number
init()
console.log(value)
function init() {
value = 1
}
TypeScript使用中的一些细节问题
interface和type
interface和type都可以描述一个对象或者函数,在大部分场景下可以混用,下面具体说一下这两者的区别
type可以interface不行
- type 可以声明基本类型别名,联合类型,元组
typescript
type name = string
type id = string|name
type Persion = [string, number]
- type 语句中还可以使用 typeof 获取实例的 类型进行赋值
typescript
let div = document.createElement('div');
type B = typeof div // type B = HTMLDivElement
interface可以type不行
- 多次声明的同名 interface 会进行声明合并,type 则不允许多次声明
typescript
interface User {
name: string
}
interface User {
age: number
}
// User 具有name和age两个属性
TypeScript类型体操案例分享
CamelCase转换
变量命名通常有下划线命名法和驼峰命名法,使用TypeScript可以实现对变量名称转化
- 下划线命名 -> 驼峰命名
typescript
type CamelCase<Str extends string> =
Str extends `${infer Left}_${infer Right}${infer Rest}`
? `${Left}${Uppercase<Right>}${CamelCase<Rest>}`
: Str;
type Res = CamelCase<'is_valid'>; // type Res = 'isValid'
- 驼峰命名 -> 下划线命名
typescript
type KebabCase<Str extends string, T extends string = ''> = Str extends `${infer L}${infer Rest}` ?
Uppercase<L> extends L ?
KebabCase<Rest, `${T extends string ? `${T}_` : ''}${Lowercase<L>}`>
: KebabCase<Rest, `${T}${L}`>
: T
type Res = KebabCase<'isValid'> // type Res = 'is_valid'
路由参数自动补全
在 inula-router 的源码中有这样的一段代码写在 inula-router 的类型声明中
typescript
// 片段1
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
// 片段2
type ParseParam<Param extends string> = Param extends `:${infer R}`
? { [K in R]: string } : {};
// 片段3
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string,
any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
// 片段4
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}`
? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>>
: ParseParam<Str>;
// 解析URL中的动态参数,以实现TypeScript提示功能
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
// Route.tsx
function Route<Path extends string, P extends Record<string, any> =
ParseURLString<ClearLeading<ClearTailing<Path>>>>(props: RouteProps<P, Path>)
片段1 用于清除字符串中多余的/
typescript
type Res = ClearLeading<"///hello"> // "Hello"
type Res2 = ClearTailing<"world///"> // "world"
片段2 使用infer
的字符串匹配能力提取url中的参数
typescript
type Res3 = ParseParam<":id"> // {id: string}
type Res4 = ParseParam<"id"> // {}
片段3 是一个用于合并对象的工具函数,合并后键值的类型均为只读的 string ,符合路由中参数的类型。
typescript
type Res5 = MergeParams<{a:string}, {b:string}> // {readonly a?: string, readonly b?: string}
type Res6 = MergeParams<{a:string,b:string}, {c:number}> // {readonly a?: string, readonly b?: string, readonly c?: string}
片段4 该函数接受一个字符串参数,如果该参数中存在 / ,则用第一个 / 把字符串分为 Param 和 Rest 两部分,将 Param 部分传入 ParseParam 函数中,去除 Rest 开头的 / 后,进行递归调用,并使用工具函数 MergeParams 将 Param 部分解析出的结果进行合并。递归的出口就是接受一个不包含 / 的字符串,返回从该字符串解析出的对象。
typescript
type Res7 = GetURLParams<"/home/:name/:id"> // {name?: string, id?: string}
通过上面这样的TypeScript类型体操再结合泛型的使用就可以实现下图中的效果了,可以在开发者使用时给出更为智能的补全信息: