作者:黄轩 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类型或联合类型通常需要配合类型守卫使用收窄变量的类型后使用。
类型守卫通常有
intypeofinstanceofArray.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类型体操再结合泛型的使用就可以实现下图中的效果了,可以在开发者使用时给出更为智能的补全信息: