Ts 基础入门指南
前言
内部推动力
今年我们部分内部大前端要开始搞 H5 的组件化,用于部门内部公有能力的沉淀以及对外输出,而组件化说到底就是做两件事
- 组件的抽离
- 文档的建设
其中组件的抽离除了要求高内聚低耦合之外,我们还需要有能对外提供完整的类型声明能力,所以需要我们学习一些 ts 相关的知识同时客户端的同学后续可能有鸿蒙应用的工作预研,而鸿蒙应用开发的框架 ArkTs 实际上就是 ts 的超集
社区情况
另外就是目前前端生态,基本上近几年内新出的库、框架,大部分都采用 ts 实现
比如 vue2,2.7 之前的版本使用 flow 做类型提示工具,在 vue2.7 版本(也是 vue2 最后一个版本)也全面采用 ts 重构,vue3 更是直接使用 ts 实现
ts 基础使用
备注: ts 官网的中文支持不是很好
搭建 ts 开发环境
如何创建一个 ts 项目的环境
mkdir demo & cd demo
pnpm init ts-demo
// 安装 ts 工作环境
pnpm i typescript @types/node ts-node -D
// 初始化 ts 配置文件
tsc --init
安装这 3 个包的作用:
- 安装 typescript 是为了提供 ts 的编译环境,typescript 这个安装包里面提供了 es5、es6、es2015-2023、esnext 等的声明文件,用于使用指定版本的 ts 编译器来将 ts 编译为 js 代码
执行之后, 进入 node_modules/typescript/lib
目录下
可以看到 es5.d.ts、es6.d.ts、es2015.d.ts、es2016.d.ts
等文件
打开 es5.d.ts, 可以看到里面为我们提供了所有的 js 数据类型的声明提示
也就是我们在 ts 中定义一个变量后,书写点的时候会自动提示的内容
// ...
interface StringConstructor {
new (value?: any): String
(value?: any): string
readonly prototype: String
fromCharCode(...codes: number[]): string
}
declare var String: StringConstructor
interface Boolean {
valueOf(): boolean
}
interface BooleanConstructor {
new (value?: any): Boolean
<T>(value?: T): boolean
readonly prototype: Boolean
}
declare var Boolean: BooleanConstructor
// ...
- @types/node 是为了在提供 node 环境所需的一些类型提示
- ts-node 提供了在终端直接运行 ts 文件的功能
执行 ts 文件
- 直接在终端 运行 tsc 文件地址.ts, 生成编译后的 js 文件,然后 执行 js 文件
(tsc 是 typescript 提供的能力)
tsc index.ts // index.js
tsc src.index.ts // src/index.js
node index.js
node scr/index.js
- 安装 ts-node ,终端直接运行 ts-node 文件名.ts
ts-node index.ts
ts-node src/index.ts
如果出现以下报错,一般有两种原因
- ts-node 版本和本地 node 不匹配
- 没有配置环境变量(window)
ts 使用过程中常见的问题: TypeScript FAQs
- 安装 ts-node 搭配插件 code runner,可以在 vscode 中直接运行 ts 文件
备注: 鸿蒙的 ets 目前 vscode 不支持高亮提示,语法提示,只能用官方提供的编辑器
开发工具中查看类型提示
【备注】:type 和 interface 在 vscode 中的显示差异
如以下代码, Person 和 Person2 除了一个使用 type、一个使用 interface,其余字段都一样
interface Person {
name: string
age: number
sex?: 'man' | 'woman'
}
type Person2 = {
name: string
age: number
sex?: 'man' | 'woman'
}
const a: Person
const b: Person2
那么在 vscode 中鼠标悬浮的提示信息分别是什么样的呢?
interface 只能显示了对应的类型声明,而 type 除了类型声明外,还能看到里面的具体字段定义
如果想要在 interface 上悬浮看到和 type 一样的效果,可以下面操作,效果如图:
- window:ctrl + 鼠标悬浮
- mac:command + 鼠标悬浮
ts 基本语法
基本语法
不同于 java,java 实在 变量前声明类型
int a = 1
String b = '1'
ts 的类型声明方式是 声明变量方式 变量名称:变量类型
具体使用往下看
基础数据类型
const num: number = 1
const str: string = '李云龙'
const bol: boolean = false
const _null: null = null
const und: undefined = undefined
const sym: symbol = Symbol(20)
const big: bigint = BigInt(20000000000000000000)
any 和 unknow
- any:任意类型,
可以被任意类型赋值,也可以赋值给任意类型
- unknow:不确定类型,
可以被任意类型赋值,但不能赋值给任意类型
let a: any = 1
let b: unknown = 2
let c: string = '3'
a = c // ✅
a = true // ✅
a = null // ✅
c = a // ✅
b = c // ✅
b = a // ✅
b = true // ✅
c = b // ❌
c = b as string // ✅
备注:
- unknow 与 任何类型的联合类型都是 unknow
- 被 unknow 类型约束的变量,不能访问其任何属性
enum 枚举
- 数字枚举
// 数字枚举
enum Color1 {
red, // 0
green, // 1
yellow, // 2
}
const f: Color1 = Color1.red
enum Color2 {
red = 3, // 3
green, // 4
yellow, // 5
}
const g: Color2 = Color2.red
enum Color3 {
red, // 0
green = 10, // 10
yellow, // 11
}
const l: Color3 = Color3.red
- 字符串枚举
enum Color4 {
red = 'red',
green = 'green',
yellow = 'yellow',
}
- 混合枚举, 即同时存在数字类型和字符串类型
对于混合枚举,数字类型的字段要么放在最前面,要么必须手动指定对应的枚举值
enum Color5 {
red, // 0
green = 'green', // green
yellow = 'yellow', // yellow
}
// 数字类型枚举必须在最前面或者指定对应的枚举值
enum Color6 {
green = 'green', // green
red, // ❌
yellow = 'yellow', // yellow
}
enum Color7 {
green = 'green', // green
red = 1, // 0
yellow = 'yellow', // yellow
}
void 和 never
前提
const fn = () => {}
console.log(fn()) // undefined
const fn2 = () => {
throw new Error()
}
console.log(fn2())
const fn = (): void => {} // ✅
const fn = (): never => {} // ❌
const fn2 = (): never => throw new shiy // ✅
即如果函数没有返回值的情况下,默认是会返回 undefined,所以类型声明使用 void,never 主要用于对于一些我们明确的知道不可能的情况,例如一个只会抛出异常的函数
备注:never 是 unknow 的子类型
object 和 Object 使用区别
- object :
只能定义 数组、对象、函数等复杂数据类型,不能给基本数据类型定义
const arr: object = [] // ✅
const obj: object = {} // ✅
const fn: object = () => {} // ✅
const asyncFn: object = async () => {} // ✅
const map: object = new Map() // ✅
const set: object = new Set() // ✅
const weakMap: object = new WeakMap() // ✅
const weakSet: object = new WeakSet() // ✅
const promise: object = Promise.resolve() // ✅
- Object :
变量的构造函数类型
,对于 js 来说,一切皆变量,所以大写 Object约束的变量要求其构造函数必须是 Object 类型
,所以可以给处理 null 和 undefined 之外的任意类型定义
const a: Object = 1 // ✅
const b: Object = '2' // ✅
const c: Object = null // ❌
const d: Object = undefined // ❌
同理, string 和 String、boolean 和 Boolean 等的区别就很容易区分了,小写的用于约束当前变量,大写用于约束变量的构造函数必须是指定的类型
日常开发使用的时候尽量不要使用大写的类型,这时官网文档上特意标注的
注意: js 中我们经常会这么写, 如下代码:
const a = {}
a.a1 = 1
a.a2 = '213'
在 ts 中,这样的写法是被禁止的,因为 ts 的隐式类型推导,会把 a 的类型声明为一个没有任何属性的对象
如果想要动态添加属性,需要指定 a 的类型为
const a: any = {}
const a = {} as any
const a: Record<string, any> = {}
const a: { [k: string]: any }
数组约束
// 简单数组
const arr1: number[] = [1, 2, 3]
const arr2: Array<number> = [1, , 2, 3]
// 不确定类型数组
const arr3: any[] = []
const arr4: unknow[] = []
// 元组
const arr3: [number, string] = [1, '2']
对象数组
interface User {
name: string
age: number
}
const userList: User[] = []
const userList2: Array<User = []>
对象定义
确定所有属性对象
type User = {
name: string
say(): string
}
const liyunlong: User = {
name: '李云龙',
say: () => '想当年也是十里八乡的俊后生',
}
假设一个变量,我们可以确定它有 name 属性和 age 属性,其他属性不确定类甚至不确定有没有其他属性对于这种类型的对象怎么定义
type Obj1 = {
name: string,
age: number
// 确定存在其他属性, 但不确定其他属性的类型
[k in string]: any
}
type Obj2 = {
name: string,
age: number
// 不确定是否存在其他属性
[k: string]?: any
}
// 普通对象的 key 只能是 string | number | Symbol 这 3 种类型之一
// 当然,如果是给 map 定义的话,还可以
// [key in any]: string
// typescript 3.x 之后的版本不建议这么写,而是建议使用联合类型
type Test1 = {
name: string
age: number
}
type Test2 = {
[k in string]: any
}
type Obj1 = Test1 & Test2]
类型断言和类型保护
- 类型断言
即变量存在多个类型的可选性,编译器无法判断具体使用哪一个类型来对变量进行约束,所以只能使用变量多个类型共同拥有的属性对变量进行约束。但对于某些情况下我们是能明确知道变量是那个类型,所以我们可以通过断言告诉编译器,变量当前是哪一个类型,以获取正确的类型约束
如下情况:
const a: any = '山本'
const len: number = a.length // ❌ 例如 null 就没有 length 属性
const len: number = (a as string).length
const len: number = (<string>a).length
断言的类型:
- 值断言
- 非空断言
- 不可变数据断言
- 强制断言 / 双重断言
// 值断言
const a: any = '1'
const len = (a as string).length
// 非空断言
function test(a: string, b?: string) {
const bLength = b!.length
}
// 不可变数据断言
const user = {
name: '李云龙',
age: 40,
} as const
// 不使用 as const --> { name:string, age: number}
// 使用 as const --> { name: '李云龙', age: 40 }
// ---> 相当于
type user = {
readonly name: '李云龙'
readonly age: 40
}
// 强制断言
const a = 1
const b: string
b = a // ❌
b = a as unknow as string // ✅
使用案例:
type HasSrcEle = HTMLImageElement | HTMLAudioElement | HTMLVideoElement
type HasHrefEle = HTMLLinkElement
window.addEventListener('error', (e: ErrorEvent) => {
const { target } = e
if (!target) { // 普通 js 执行异常
// ...
} else { // 资源加载异常
if ((target as HasSrcEle).src) {
console.log('报错的节点是包含 src 属性的标签, 出错的资源链接是',
(target as HasSrcEle).src
);
} else ((target as HasHrefEle).href) {
console.log('报错的节点是包含 href 属性的标签,出错的资源链接是'
(target as HasHrefEle).href
)
} else {
console.log('报错的节点是其他标签')
}
})
备注:类型断言被认为是有害的,所以在使用的时候能不用就尽量不用,例如下面案例的断言就是会出问题的 window.addEveneListener('error', (e: ErrorEvent) => (e as HTMLElement).tagName)
- 类型保护
如上面的代码,我们对 target 断言为某一类型的,这样后续使用就不要一直使用断言了
type HasSrcEle = HTMLImageElement | HTMLAudioElement | HTMLVideoElement
type HasHrefEle = HTMLLinkElement
function isHasSrcEle(ele: EventTarget): ele is HasSrcEle {
return 'src' in ele
}
function isHasHrefEle(ele: EventTarget): ele is HasHrefEle {
return 'href' in ele
}
window.addEventListener('error', (e: ErrorEvent) => {
const { target } = e
if (!target) {
// ...
} else {
if (isHasSrcEle(target)) {
console.log('报错的节点是包含 src 属性的标签, 出错的资源链接是', target.src)
} else if (isHasHrefEle(target)) {
console.log('报错的节点是包含 href 属性的标签,出错的资源链接是', target.href)
} else {
console.log('报错的节点是其他标签')
}
}
})
一般的组件库或者开源工具(vue3,ahooks,vueuse 等)都会定义一些基础的类型保护函数, 如下:
function isString(value: any): value is string {
return typeof value === 'string'
}
function isNumber(value: any): value is number {
return typeof value === 'number'
}
function isBoolean(value: any): value is boolean {
return typeof value === 'boolean'
}
function isFunction(value: any): value is Function {
return typeof value === 'function'
}
function isObject(value: any): value is object {
return typeof value === 'object' && value !== null
}
function isArray(value: any): value is any[] {
return Array.isArray(value)
}
function isPromise(value: any): value is Promise<any> {
return value instanceof Promise
}
function isDate(value: any): value is Date {
return value instanceof Date
}
接口约束 interface
interface User {
name: string
age?: number
readonly sex: '1' | '2' // 只读属性,编辑结果就是修改变量的 writeable 属性为 false
}
interface Fn {
(a: string, b: number, ...args: any[]): void
}
interface ChineseUser extends User {
alias: '炎黄子孙'
}
interface ChineseUser2 extends User1 {
alias: '炎黄子孙'
}
type ChineseUser3 = User1 & {}
类型别名 type
类型别名,即用于指代一个或多个类型的约束大部分情况下的使用和 interface 一致,但不能使用 extends
- type 不能继承 type
- interface 可以继承 type、也可以继承 interface
所以当我们定义的类型约束后续后扩展需求的时候,建议使用 interface,如果确定类型约束不会发生变化的,则两者都可以使用
type A = string
type B = string | number
type C = 'up' | 'down'
type fn = () => void
type Obj = {
name: string
age: number
sex?: '1' | '2'
}
interface Obj2 {
name: string
age: number
sex?: '1' | '2'
}
let a: Obj
let b: Obj2
a = b
b = a
字面量类型
- 字符串字面量
- 数字字面量
type One = 1
type OneTwoThree = 1 | 2 | 3
type LaoLi = '李云龙'
const one: One = 2 // ❌, 只能是 1
const laoli: LaoLi = '楚云飞' // ❌, 只能是 李云龙
同名合并(同名类型合并)
interface A {
a: number
}
interface A {
b: number
}
// interface A {
// a: number
// b: number
// }
注意:
- 两个同名约束的同个属性,其类型约束必须完全一致, 如果第二个类型 A 也有 a 属性且类型不是 number,ts 就会提示报错
- 类型别名不支持同名合并
interface B {
a: number
}
interface B {
a: string
}
同名合并的实际应用场景:
例如 app 内嵌的 H5 页面,window 上除了默认的属性方法外,客户端还可能会注入一些方法,例如 initBridge 方法,那么我们就可以通过同名合并的方式,让 window 上的属性方法都具备 initBridge 方法
我们给原有的构造函数新增属性,如 Date。propotype.format = (str) => ...
interface Window {
initBridge: () => void
}
interface Date {
format: (str: string) => string
}
函数约束和函数重载
- 函数约束
- 参数约束
- 返回值约束
type Fn1 = (a:number, b?: string) => number
const fn1: Fn1 = (1) => 1
const fn2: Fn1 = (1, '2') => 2
type Fn2<T> = () => Promise<T>
const fn2: Fn2<number[]> = () => Promise.resolve([1, 2])
- 函数重载
// 声明:
function add(arg1: string, arg2: string): string
function add(arg1: number, arg2: number): number
// 实现:
function add(arg1: string | number, arg2: string | number) {
// 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2
if (typeof arg1 === 'string' && typeof arg2 === 'string') {
return arg1 + arg2
} else if (typeof arg1 === 'number' && typeof arg2 === 'number') {
return arg1 + arg2
}
}
函数重载的匹配规则是从上往下,所以我们在写函数重载的时候,把最经常使用的类型写着最上面,最下面的一般是兜底操作
函数重载的 3 个注意点(来自官网): https://www.tslang.cn/docs/handbook/declaration-files/do-s-and-don-ts.html
重载与回调函数
函数重载书写顺序
不要因为末尾参数不同写不同的重载
继承和条件判断
type NumOrStr<T> = T extends string ? string : number
type TorNever<T> = T extends any ? T : never
// 获取一个对象中所有函数的类型
type forInFn<T> = {
[k in keyof T]: T[k] extends () => void ? T[k] : never
}[keyof T]
类约束
- implements 实现类声明
- abstract 抽象类,只能被继承,不能被继承实例化
- 类的内部属性和方法的约束
- implement 实现类声明
type IUser = {
name: string
age: number
say(): void
}
class User implements IUser {
name: string
age: number
say() {}
static a: number
private b: number
public c: number
}
- implements 实现,一个新的类,从父类或者接口实现所有的属性和方法,同时可以重写属性和方法,包含一些新的功能
- extends 继承,一个新的接口或者类,从父类或者接口继承所有的属性和方法,不可以重写属性,但可以重写方法
- abstract 抽象类
抽象类是可以派生其他类的基类。它不能被直接实例化。与接口不同,一个抽象类可以包含它的成员的实现细节
abstract class Animal {
type: string = 'animal'
}
const a = new Animal() // 报错
class Person extends Animal {
static testName = 'Person'
public name: string = '11'
private age: number = 10
protected gender: string = 'male'
}
- 类属性方法的约束
- static: 静态属性,es6 自带的属性,只能类访问,实例不能访问
- private:私有属性,只能在类中被访问,不能被继承的子类和实例使用
- protected: 被保护属性,只能在类中和被继承的子类中访问,实例不可访问
- public:公有属性,实例和继承的子类都可访问
实例
class Person {
static testName = 'Person'
public name: string = '11'
private age: number = 10
protected gender: string = 'male'
}
class Student extends Person {
public study(): void {
console.log(`${this.name} is studying.`)
}
}
const p = new Person()
const s = new Student()
泛型
主要用于一些通用的数据结构类型里面存在不确定类型的类型声明中使用案例一:假设接口返回的数据格式如下,不同接口只有 data 字段数据不确定,其余均确定
{
code: 200,
msg: 'xxxx',
data: xxx
}
那我们怎么使用 ts 来为我们的接口定义呢?
interface IResponce<T> {
code: number
msg: string
data: T
}
const http = async <T>(url: string): Promise<IResponce<T>> => {
return (await fetch(url)).json()
}
interface IUser {
id: string
name: string
age: number
avatar: string
}
const getUserList = async () => {
const url = 'xxx/xxx/userList'
const res = await http<IUser[]>(url)
const userList = res.data
return userList
}
案例二:async-to.js
const to = <T, U = Error>(fun: Promise<T>, err?: U): Promise<[null, T] | [U, null]> => {
return fun.then<[null, T]>((res: T) => [null, res]).catch<[U, null]>((error: U) => [err ?? error, null])
}
const fn = async () => fetch('xxxx').josn()
const [err, res] = await to<User[]>(fn())
联合类型和交叉类型
- 联合类型,即变量可以符合多种约束中的其中一种
type Test = string | number | false
注意联合类型在函数内部使用,大部分情况需要搭配类型断言使用,即前面说到的类型保护
- 交叉类型
type A = string | number
type B = boolean
type AB = A & B // string | number | boolean
type A1 = {
a: string
b?: number
}
type B1 = {
a: number
b: number
c: string
}
type AB1 = A1 & B1
// type AB1 = {
// a: never 每一一个数据可以既是 string 类型又是 number 类型,所以是 never
// b: number
// c: string
// }
可辨识联合类型
特征:
- 多个接口或类型别名实现的联合类型
- 每个接口或别名都具有一个或多个相同的属性类型
interface User {
type: 'User'
name: string
age: number
}
interface Box {
type: 'Box'
height: number
width: number
}
interface Tree {
type: 'Tree'
top: number
}
type Test = User | Box | Tree
function test(a: Test) {
switch(a.type) {
// ts 可以自动识别对应类型, 但是注意要把全部类型都罗列出来
// 如果有缺少的就会提示报错
case 'User':
return a.name
break
case 'Box':
return a.height
break
case 'Tree'
return a.top
break
default
return null
break
}
}
keyof 和 typeof
- keyof
type Test = {
a: string
b: boolean
c: number
}
type TestKeys = keyof Test
// type TestKeys = 'a' | 'b' | 'c'
- typeof
const TestMap = {
up: 'up',
down: 'down',
} as const
type ITestMap = typeof TestMap
// type ITestMap = {
// up: string;
// down: string;
// }
type TestMapVal = ITestMap[keyof ITestMap]
// type TestMapVal = "up" | "down"
const fn3 = async () => {
const res = (await fetch('xxxxx')).json()
return res
}
type IFn = typeof fn3
// type IFn = () => Promise<any>
实际使用场景
const Liyunlong = {
name: '李云龙',
shengfen: '独立团团长',
}
type Li = typeof Liyunlong
function getValue(a: Li, k: string) {
// ❌
return a[k]
}
function getValue1(a: Li, k: keyof Li) {
// ✅
return a[k]
}
in 属性遍历
type Obj = {
name: string
age: number
}
// 将 Obj 的全部属性变成只读属性
type MyReadOnly<T> = {
readonly [K in keyof T]: T[K]
}
type Obj1 = MyReadOnly<Obj>
// 将 Obj1 的属性全部变成可选属性
type MyNoRequired<T> = {
[K in keyof T]?: T[K]
}
type Obj2 = MyNoRequired<Obj1>
// 将 Obj2 的全部属性变成必须且可修改的属性,
type MyNoReadOnly<T> = {
-readonly [k in keyof T]-?: T[k]
}
infer 类型推导
infer 类型推断,可以理解为定义了一个变量使用常见:例如对于防抖函数的封装:
type Debounce = (fun: Function) => Function
因为这个函数 fun 的参数是不固定的,所以我们一般有两种方式去获取
- 使用泛型
- 使用 Parameter 和 ReturnType(这两个工具都是基于 infer 实现的, 基本上开源的所有第三方工具库的防抖函数都是用这种方式声明的,包括 lodash、underscroe)
// 泛型实现
type Debounce<T> = (fun: T) => T
// parameter + returnType 实现,我们看 lodash 的类型声明
interface DebouncedFunc<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T> | undefined
cancel(): void
flush(): ReturnType<T> | undefined
}
其他场景:
type Word = 'word'
type HelloWord = `hellow ${Word}` // hello word
type NumToStr<T extends number> = `${T}`
type FiveStr = NumToStr<5> // '5'
type PickValue<T extends any> = `${infer R}%` ? R : unkonwn
type PickFivePercent = PickValue<'5%'> // 5
type Test<T> = T extends (a: infer R, b: infer R) ? R : any
type TestA = Test<{ a: string, b: string}> // string
type TestB = Test<{ a: 'hello', b: number[]}> // 'hello' | number[]
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
type IReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never
类型声明文件
声明文件这里官方文档用了一个很大的篇幅来说明,这里我就简单的说一下,详细内容可以到官方文档查看 ts 声明文件
第三方库的声明文件可以这么分类:
- 库本身使用 ts 编写
- 一般在库打包的时候,会使用 rollup-plugin-dts 或者 webpack-plugin-dts 生成 xxx.d.ts 的文件
- 库本身是由 js 编写
- 需要额外创建一个类型声明库,如 jquery ---> @ types/jquery - AMD 格式 - CJS 或者 ESM 格式 - 如果是 UMD 的库的化,他的声明文件一般会有两个,按情况使用对应的声明文件
AMD 全局库,例如 jquery 假设现在我们引入了一个第三方包,里面的代码如下,那我们该怎么为它做一个类型声明文件呢? test.js
;(() => {
function setTitle(title) {
document && document.title !== title && (document.title = title)
}
function getTitle() {
return document && document.title
}
let title = getTitle()
})()
test.html 中引入 test.js
<script src="./test.js"></script>
test.d.ts
// declare: 用于声明全局类型,即整个项目都可以使用
declare function setTitle(title: string | number): void
declare function getTitle(): null | string
declare let title: string
ESM / CJS 模块化库, path、http、express 等 test.js
function setTitle(title) {
document && document.title !== title && (document.title = title)
}
function getTitle() {
return document && document.title
}
module.exports = {
setTitle: setTitle,
getTitle: getTitle,
}
test.d.ts
// 导出模块类型
export = Test
// 声明一个全局模块
declare module Test {
function setTitle(title: string): void
function getTitle(): string
}
export function setTitle(title: string): void
export function getTitle(): string
备注:目前社区 95% 的开源库都有相应的类型声明文件库了,一般以 @types/xxx 如:
- lodash --> @types/lodash
- juquery --> @types/juquery
具体的声明文件模板: ts 官网声明文件模板
ts 声明规范
- 类型定义规范
ts 社区常用的类型声明一般有两种声明规范
- 以大写字母 I 开头,一般项目内部,自定义的类型声明会有这种写法,使用 I 开头一般是为了避免出现同名的类
- 直接首字母大写,一般 @types/xxxx 里面的类型都是这种写法
type IUser = {
//...
}
type User = {
//...
}
class User implement IUser {} // 这种情况一般就是使用 IUser,避免混淆
const user1: IUser = new User()
const user2: User
使用哪种方式都先,但是一个项目里面尽量使用统一的规范
- 文件名规范
类型声明文件的定义目前社区也有两种比较主流的规范
- src 下定义一个 types 或者 interface 的文件夹
src - components - componentA - types - global.d.ts - components - componentA.ts
- 类型声明文件跟随文件
src - components - componentA.ts - componentA.type.ts - global.d.ts
ts 进阶使用
装饰器
什么是装饰器,可以理解为一个高阶函数或者可以近似的比作 java 里面的类型注解 Decorator 是 ES7 的一个新语法,目前仍处于第 2 阶段提案中,正如其“装饰器”的叫法所表达的,他通过添加@方法名(函数名)可以对一些对象进行装饰包装然后返回一个被包装过的对象,可以装饰的对象包括:类,属性,方法等。
- Q :什么是 ts 装饰器?
- 文心一言:
装饰器(Decorator)是一种设计模式
,它提供了一种动态地添加职责到对象的方法,而这些职责在编译时期是不存在的。这和静态类型的语言(如 Java 或 C#)中的接口或继承不太一样。装饰器模式在运行时决定要给一个对象添加哪些职责,它可以在不修改对象结构的情况下,动态地给对象增加一些新的功能。控制反转(Inversion of Control,IoC)是一个设计原则
,它用于降低代码之间的耦合度。在传统的程序设计中,程序的流程控制直接依赖于对象的创建和管理。控制反转的设计原则将这个流程控制反转,使得对象的创建和管理不再由程序直接控制,而是通过第三方容器(IoC 容器)来管理。这样,程序中的各个部分就解耦了,使得代码更加灵活、可维护和可测试。依赖注入(Dependency Injection,DI)是实现控制反转的一种常见手段
。它是一种设计技术,用于解决程序中的耦合问题。依赖注入的核心思想是将依赖关系从硬编码中解放出来,将其交给第三方容器(IoC 容器)来管理。这样,对象的创建和依赖关系的配置可以在运行时动态地决定,使得代码更加灵活、可扩展和可测试。
有哪些前端框架使用了装饰器
- nestjs - 基于 express 实现
- 阿里开源的 midwayjs, 基于 eggjs 升级(eggjs 是封装了 koa)
- BFF (Backend-for-Frontend)服务于前端的后端, BFF(Backend-for-Frontend)中间层深入解析
- severless, 云开发,云函数(目前一般是搭配小程序使用)
- 纯服务端,接口测试工具 apifox 的后端就是使用 midway 实现的
- 华为鸿蒙 -ArkTs 3.x 之后的版本
- Mobx5
装饰器分类
- 类装饰器: ClassDecorator
- 函数装饰器: MethodDecorator
- 参数装饰器: ParameterDecorator,
- 属性装饰器: PropertyDecorator
装饰器由于在 js 中任处于提案状态,所以 ts 的装饰器需要手动在 tsconfig.json 中打开一个配置项才能支持
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
装饰器的类型定义是独立于 lib.es5.d.ts
- /node_modules/typescript/lib
- lib.decorators.d.ts
- lib.decorators.legacy.d.ts
只有开启上述两个配置才会把装饰器的声明文件导入到基础声明文件中看下 lib.decorators.legacy.d.ts 这个文件:
// 类装饰器
declare type ClassDecorator = <TFunction extends Function>(
target: TFunction, // 被装饰的类
) => TFunction | void
// 属性装饰器
declare type PropertyDecorator = (
target: Object, // 被装饰的类
propertyKey: string | symbol, // 类里面被装饰的属性
) => void
// 函数装饰器
declare type MethodDecorator = <T>(
target: Object, // 被装饰的类
propertyKey: string | symbol, // 类里面被装饰的函数名
descriptor: TypedPropertyDescriptor<T>, // 被装饰函数的属性描述符
) => TypedPropertyDescriptor<T> | void
// 参数装饰器
declare type ParameterDecorator = (
target: Object, // 被装饰的类
propertyKey: string | symbol | undefined, // 参数所在函数的名字
parameterIndex: number, // 被装饰的参数,在函数入参中的下标
) => void
按 ts 的规范来看,目前函数装饰器和参数装饰器是可以独立在类外面使用的,但是我自己测试的时候发现总会有各种奇奇怪怪的问题
类装饰器
类装饰器的类型定义
declare type ClassDecorator =
<TFunction extends Function>(target: TFunction) => TFunction | void
可以看出,类装饰器实际就是一个函数,这个函数接受一个参数,这个参实际上就是具体被装饰的类案例:
const setName: ClassDecorator = (target) => {
target.prototype.personName = '李云龙'
console.log('setName2')
}
@setName
class Person {
constructor() {
console.log('Person')
}
getPersonName() {
console.log('getPersonName', this.personName)
}
}
const p = new Person()
p.getPersonName()
上面代码声明了一个类和装饰器,装饰器给类新增了一个属性 personName,但是实际上这个类实例化之后的对象拿不到这个新增的属性
这时,就可以利用【同名合并】我们可以解决这个问题,即代码中补充这样一个类型声明
interface Person {
personName: string
}
比如 ArkTs 的自定义组件的装饰器就是利用这个实现的,如下: ArkTs 官网代码片段
@Component
struct HelloComponent {
@State message: string = 'Hello, World!';
build() {
// HelloComponent自定义组件组合系统组件Row和Text
Row() {
Text(this.message)
.onClick(() => {
// 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
this.message = 'Hello, ArkUI!';
})
}
}
}
ArkTs 提供的一些内置装饰器;
- @Component 组件装饰器
- @Entry 入口装饰器
- @State 状态装饰器
- @Link 双向数据绑定装饰器,类似 vue 的 v-model
- @Builder 自定义构造函数装饰,类似 vue 的 h 函数
- @Prop 单项数据流装饰器
- @Watch 装饰器, 类似 vue 的 watch 和 watchEfffect
- @Provider + @Consume 提供者装饰器、消费者装饰器,用于跨层级数据传递
- @CustomDiaLog 自定义弹窗装饰器
- @Extends 组件扩展样式装饰器
- @Styles 封装属性样式装饰器
- ...
属性装饰器
类型定义
// 属性装饰器
declare type PropertyDecorator = (
target: Object, // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
propertyKey: string | symbol 成员的名字
) => void;
注意:
- 装饰器不能出现在 xx.d.ts 的文件,也不能出现在任何外部上下文中。
- 属性描述符不会做为参数传入属性装饰器,这与 TypeScript 是如何初始化属性装饰器的有关。因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
const say: PropertyDecorator = (target: Object, propertyKey: string | symbol) => {
// 这里也可以做一些响应式数据的依赖收集
let value: any
const getter = () => {
console.log('getter', value)
return value
}
const setter = (newVal: any) => {
console.log('setter', newVal)
value = newVal
}
// vue2 响应式数据的依赖收集就是利用这个 api
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
})
}
class Person {
@say name: string
constructor() {
this.name = '李云龙'
}
getPersonName() {
return this.name
}
setPersonName(newVal: string) {
this.name = newVal
}
}
const p = new Person()
p.getPersonName()
p.setPersonName('楚云飞')
输出结果:
setter 李云龙
getter 李云龙
setter 楚云飞
这就是属性装饰器的作用,比如 ArkTs 的响应式变化,就是在 @state 里面对变量进行了一些额外处理
函数装饰器
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>,
) => TypedPropertyDescriptor<T> | void
函数装饰器和参数装饰器也都是类似的实现方式,这里就不细讲了,直接放一段 midwayjs 官网代码片段看下就行:
// src/controller/home.ts
import { Controller, Get, Post } from '@midwayjs/core'
@Controller('/')
export class HomeController {
@Get('/') // 访问 / 接口执行这个方法
async home() {
return 'Hello Midwayjs!'
}
@Post('/update') // 访问 /update 这个接口执行
async updateData() {
return 'This is a post method'
}
}
参数装饰器
略
装饰器工厂
上面写的类装饰器和属性装饰器都有一个问题,就是我们没法自定义装饰器里的内容,比如前面的类装饰器里面的 personName 默认值,这时就需要使用工厂模式来实现我们需要的装饰器
interface Person {
personName: string
}
const setName = (name: string) => {
return (target: any) => {
target.prototype.personName = name
console.log('setName2')
}
}
@setName('山本')
class Person {
constructor() {
console.log('Person')
}
getPersonName() {
console.log('getPersonName', this.personName)
}
}
const p = new Person()
p.getPersonName()
组合装饰器
有时候,对与一个变量,我们可以需要多个装饰器对他进行同时修饰,对于使用多个装饰器装饰一个变量(类、函数、方法、属性)的行为,一般被称为组合装饰器
普通装饰器组合
装饰器工厂组合
混合装饰器组合
组合装饰器执行顺序:
- 普通装饰器组合:从下往上执行
- 装饰器工厂组合:先从上往下执行工厂函数,得到全部装饰器,在从下往上执行
- 混合装饰器组合:先执行工厂装饰器,吧返回的装饰器放在对应位置,然后从下往上执行
普通装饰器组合
- 装饰器工厂组合
- 混合装饰器
@setName()
@setAge
@setTitle()
@setCity
--> 下划线假设是返回的装饰器
@_setName
@setAge
@_setTitle
@setCity
ts 的内置工具
interface Person {
name: string
age: number
sex?: 'man' | 'woman'
}
注意:下面说的几个工具类,定义的方式都可以在 lib.es5.d.ts 这个文件内找到
Record
type Obj = {
[k: string]: string
}
type Objb2 = Record<string, string>
// 实际上 Obj 和 Obj2 的约束能力是一样的
type T1 = {
name: Person
age: Person
}
type T2 = Record<'name' | 'age', Person>
// type T = {
// name: Person;
// age: Person;
// }
实现
type Record<T extends keyof any, U> = {
[K in T]: U
}
Readonly
将变量变为只读变量
type T6 = Readonly<Person>
// type T6 = {
// readonly name: string;
// readonly age: number;
// readonly sex?: "man" | "woman" | undefined;
// }
实现
type Readonly<T> = {
// 这里的 in,可以理解为是使用 for in 去实现的
readonly [K in keyof T]: T[K]
}
readonly 编辑的结果,实际上就是修改对象的 writeable 属性为 false,类型下面的代码:
Object.defineProprty({}, 'name', {
value: '李云龙',
writeable: false,
})
ReadonlyArray
作用:把一个数组变成只读数组,包括数组原型链上的方法和 readonly 的区别, 如下,使用 断言 和 readonly 进行约束之后的数组,虽然无法直接修改,但是还是可以通过原型链上的方法去修改的
const arr11 = [1, 2] as const
arr11.forEach((item) => {
item++
})
const arr22: readonly number[] = [1, 2]
arr22.forEach((item) => {
item++
})
代码比较多,建议看源码,这里只挑一部分源码
interface ReadonlyArray<T> {
// ...
forEach(
callbackfn: (value: T, index: number, array: readonly T[]) => void,
thisArg?: any
): void
map<U>(
callbackfn: (value: T, index: number, array: readonly T[]) => U,
thisArg?: any
): U[]
//...
}
Partial
作用:创建一个新的类型声明,属性全部变成可选
type T1 = type T1 = Partial<Person>
// type T1 = {
// name?: string | undefined;
// age?: number | undefined;
// sex?: "man" | "woman" | undefined;
// }
实现
type Partial<T> = {
[K in keyof T]?: T[k]
}
Required
作用:创建一个新的类型声明,属性全部变成必选
type T2 = Required<Person>
// type T2 = {
// name: string;
// age: number;
// sex: 'man' | 'woman';
// }
实现
type Required<T> = {
[K in keyof T]-?: T[k]
}
Pick
作用: 提取出指定的类型作为一个新的类型声明
type T3 = Pick<Person, 'name' | 'age'>
// type T3 = {
// name: string;
// age: number;
// }
实现
type Pick<T, U extends keyof T> = {
[K in U]: T[U]
}
Exclude
TypeScript - 理清 Omit 与 Exclude 的关系与区别 作用:类型过滤
type T3 = Exclude<string | number, number> // string
实现
type Exclude<T, U> = T extends U ? never : T
即上面的 T3 可以这么理解
string extedns number ---> string
number extends number ---> never
string & never --> string
备注: T 必须是一个联合类型
Extract
作用:提取,exclude 的反逻辑
type T5 = Extract<string | number, number> // number
string extedns number ---> never
number extends number ---> number
never & number ---> number
实现:
type Extract<T, U> = T extends U ? T : never
Omit
作用: 提取出非指定的类型作为一个新的类型声明
type T4 = Omit<Person, 'name' | 'age'>
// type T4 = {
// sex?: "man" | "woman" | undefined;
// }
实现
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
Omit 和 Exclude 都是排除某几个类型声明,那它们的区别是?
- Exclude 的接受的第一个泛型必须时联合类型
- Omit 接受的第一个泛型必须是一个接口或者类型别名
Parameters
作用:获取函数的参数类型
type Parameters<T extends (...args: any) => any>
= T extends (...args: infer P) => any ? P : never
ReturnType
作用:获取函数的返回值
type ReturnType<T extends (...args: any) => any>
= T extends (...args: any) => infer R ? R : any
InstanceType
作用:获取类的实例类型
type InstanceType<T extends abstract new (...args: any) => any>
= T extends abstract new (...args: any) => infer R
? R
: any
到这 ts 的语法使用就基本讲完了,剩下的如 命名空间、三斜杠标签等开发过程基本不会用到的知识点可以到官网上查看
ts 开发的一些快速工具
- vscode 强烈建议安装这个插件 JSON to TS
- json to xxxx 网站