【前端风向标】第一期-TypeScript知识分享

作者:黄轩 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是永不返回

  1. 函数存在无法到达的终点会返回never
TypeScript 复制代码
const err = (msg: string) => { throw new Error(msg) } //err: (msg: string) => never
  1. 一定失败的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是 stringnumber的联合类型,因此a只有stringnumber共有的方法

同样的如果是两个类型的联合类型,也只能访问两个类型共有的属性

在使用联合类型时,通常要配合类型守卫缩窄对象的类型,确保类型安全

映射类型

对象、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
非空断言

当你明确知道某个值不可能为undefinednull时,你可以用 在变量后面加上一个!来告诉编译器这个变量确定不会空

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类型体操再结合泛型的使用就可以实现下图中的效果了,可以在开发者使用时给出更为智能的补全信息:

相关推荐
蘑菇头爱平底锅3 分钟前
数字孪生-DTS-孪创城市-湖区分布
前端·数据可视化
Enti7c1 小时前
css定位
前端·css
zy0101012 小时前
useEffect
开发语言·前端·javascript·react·useeffect
@PHARAOH2 小时前
WHAT - React 进一步学习推荐
前端·学习·react.js
kovlistudio2 小时前
红宝书第四十讲:React 核心概念:组件化 & 虚拟 DOM 简单教程
开发语言·前端·javascript·学习·react.js·前端框架
巴巴_羊2 小时前
React Redux
开发语言·前端·javascript
Mintopia2 小时前
Node.js 中的this
前端·javascript·node.js
Mike_jia2 小时前
一篇文章带你了解一款强大的开源跨平台远程桌面管理工具---XPipe
前端·开源
._Ha!n.2 小时前
React基础知识一
前端·react.js
Mintopia3 小时前
深入理解 Three.js 中 Shader 的使用及示例
前端·javascript·three.js