Skip to content
霞露小伙 — HfWang
On this page

Ts 基础入门指南

前言

内部推动力

今年我们部分内部大前端要开始搞 H5 的组件化,用于部门内部公有能力的沉淀以及对外输出,而组件化说到底就是做两件事

  • 组件的抽离
  • 文档的建设

其中组件的抽离除了要求高内聚低耦合之外,我们还需要有能对外提供完整的类型声明能力,所以需要我们学习一些 ts 相关的知识同时客户端的同学后续可能有鸿蒙应用的工作预研,而鸿蒙应用开发的框架 ArkTs 实际上就是 ts 的超集

社区情况

另外就是目前前端生态,基本上近几年内新出的库、框架,大部分都采用 ts 实现

比如 vue2,2.7 之前的版本使用 flow 做类型提示工具,在 vue2.7 版本(也是 vue2 最后一个版本)也全面采用 ts 重构,vue3 更是直接使用 ts 实现

完整版文章链接

ts 基础使用

备注: ts 官网的中文支持不是很好

搭建 ts 开发环境

如何创建一个 ts 项目的环境

shell
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 中定义一个变量后,书写点的时候会自动提示的内容

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 文件

  1. 直接在终端 运行 tsc 文件地址.ts, 生成编译后的 js 文件,然后 执行 js 文件

(tsc 是 typescript 提供的能力)

typescript
tsc index.ts   // index.js
tsc src.index.ts // src/index.js

node index.js
node scr/index.js
  1. 安装 ts-node ,终端直接运行 ts-node 文件名.ts
typescript
ts-node index.ts
ts-node src/index.ts

如果出现以下报错,一般有两种原因

  1. ts-node 版本和本地 node 不匹配
  2. 没有配置环境变量(window)

ts 使用过程中常见的问题: TypeScript FAQs

  1. 安装 ts-node 搭配插件 code runner,可以在 vscode 中直接运行 ts 文件

备注: 鸿蒙的 ets 目前 vscode 不支持高亮提示,语法提示,只能用官方提供的编辑器

开发工具中查看类型提示

【备注】:type 和 interface 在 vscode 中的显示差异

如以下代码, Person 和 Person2 除了一个使用 type、一个使用 interface,其余字段都一样

typescript
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 实在 变量前声明类型

java
int a = 1
String b = '1'

ts 的类型声明方式是 声明变量方式 变量名称:变量类型

具体使用往下看

基础数据类型

typescript
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:不确定类型,可以被任意类型赋值,但不能赋值给任意类型
typescript
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 枚举

  • 数字枚举
typescript
// 数字枚举
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
  • 字符串枚举
typescript
enum Color4 {
  red = 'red',
  green = 'green',
  yellow = 'yellow',
}
  • 混合枚举, 即同时存在数字类型和字符串类型

对于混合枚举,数字类型的字段要么放在最前面,要么必须手动指定对应的枚举值

typescript
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

前提

typescript
const fn = () => {}
console.log(fn()) // undefined

const fn2 = () => {
  throw new Error()
}
console.log(fn2())
typescript
const fn = (): void => {} // ✅
const fn = (): never => {} // ❌
const fn2 = (): never => throw new shiy // ✅

即如果函数没有返回值的情况下,默认是会返回 undefined,所以类型声明使用 void,never 主要用于对于一些我们明确的知道不可能的情况,例如一个只会抛出异常的函数

例如 midway 中,对于接口异常的中间件处理

备注:never 是 unknow 的子类型

object 和 Object 使用区别

  • object : 只能定义 数组、对象、函数等复杂数据类型,不能给基本数据类型定义
typescript
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 之外的任意类型定义
javascript
const a: Object = 1 // ✅
const b: Object = '2' // ✅
const c: Object = null // ❌
const d: Object = undefined // ❌

同理, string 和 String、boolean 和 Boolean 等的区别就很容易区分了,小写的用于约束当前变量,大写用于约束变量的构造函数必须是指定的类型

日常开发使用的时候尽量不要使用大写的类型,这时官网文档上特意标注的

注意: js 中我们经常会这么写, 如下代码:

javascript
const a = {}
a.a1 = 1
a.a2 = '213'

在 ts 中,这样的写法是被禁止的,因为 ts 的隐式类型推导,会把 a 的类型声明为一个没有任何属性的对象

如果想要动态添加属性,需要指定 a 的类型为

typescript
const a: any = {}
const a = {} as any
const a: Record<string, any> = {}
const a: { [k: string]: any }

数组约束

typescript
// 简单数组
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']

对象数组

typescript
interface User {
  name: string
  age: number
}

const userList: User[] = []
const userList2: Array<User = []>

对象定义

确定所有属性对象

typescript
type User = {
  name: string
  say(): string
}

const liyunlong: User = {
  name: '李云龙',
  say: () => '想当年也是十里八乡的俊后生',
}

假设一个变量,我们可以确定它有 name 属性和 age 属性,其他属性不确定类甚至不确定有没有其他属性对于这种类型的对象怎么定义

typescript
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]

类型断言和类型保护

  1. 类型断言

    即变量存在多个类型的可选性,编译器无法判断具体使用哪一个类型来对变量进行约束,所以只能使用变量多个类型共同拥有的属性对变量进行约束。但对于某些情况下我们是能明确知道变量是那个类型,所以我们可以通过断言告诉编译器,变量当前是哪一个类型,以获取正确的类型约束

如下情况:

typescript
const a: any = '山本'

const len: number = a.length // ❌ 例如 null 就没有 length 属性
const len: number = (a as string).length
const len: number = (<string>a).length

断言的类型:

  • 值断言
  • 非空断言
  • 不可变数据断言
  • 强制断言 / 双重断言
typescript
// 值断言
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 // ✅

使用案例:

typescript
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)

  1. 类型保护

如上面的代码,我们对 target 断言为某一类型的,这样后续使用就不要一直使用断言了

typescript
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 等)都会定义一些基础的类型保护函数, 如下:

typescript
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

typescript
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,如果确定类型约束不会发生变化的,则两者都可以使用

typescript
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

字面量类型

  • 字符串字面量
  • 数字字面量
typescript
type One = 1
type OneTwoThree = 1 | 2 | 3
type LaoLi = '李云龙'

const one: One = 2 // ❌, 只能是 1
const laoli: LaoLi = '楚云飞' // ❌, 只能是 李云龙

同名合并(同名类型合并)

typescript
interface A {
  a: number
}

interface A {
  b: number
}

// interface A {
//   a: number
//   b: number
// }

注意:

  • 两个同名约束的同个属性,其类型约束必须完全一致, 如果第二个类型 A 也有 a 属性且类型不是 number,ts 就会提示报错
  • 类型别名不支持同名合并
typescript
interface B {
  a: number
}

interface B {
  a: string
}

同名合并的实际应用场景:

  1. 例如 app 内嵌的 H5 页面,window 上除了默认的属性方法外,客户端还可能会注入一些方法,例如 initBridge 方法,那么我们就可以通过同名合并的方式,让 window 上的属性方法都具备 initBridge 方法

  2. 我们给原有的构造函数新增属性,如 Date。propotype.format = (str) => ...

ts
interface Window {
  initBridge: () => void
}

interface Date {
  format: (str: string) => string
}

函数约束和函数重载

  • 函数约束
    • 参数约束
    • 返回值约束
typescript
type Fn1 = (anumber, 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])
  • 函数重载
typescript
// 声明:
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

  • 重载与回调函数

  • 函数重载书写顺序

  • 不要因为末尾参数不同写不同的重载

继承和条件判断

typescript
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 抽象类,只能被继承,不能被继承实例化
  • 类的内部属性和方法的约束
  1. implement 实现类声明
typescript
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 继承,一个新的接口或者类,从父类或者接口继承所有的属性和方法,不可以重写属性,但可以重写方法
  1. abstract 抽象类

抽象类是可以派生其他类的基类。它不能被直接实例化。与接口不同,一个抽象类可以包含它的成员的实现细节

typescript
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'
}
  1. 类属性方法的约束
    • static: 静态属性,es6 自带的属性,只能类访问,实例不能访问
    • private:私有属性,只能在类中被访问,不能被继承的子类和实例使用
    • protected: 被保护属性,只能在类中和被继承的子类中访问,实例不可访问
    • public:公有属性,实例和继承的子类都可访问

实例

typescript
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 字段数据不确定,其余均确定

javascript
{
  code: 200,
  msg: 'xxxx',
  data: xxx
}

那我们怎么使用 ts 来为我们的接口定义呢?

typescript
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

typescript
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])
}
typescript
const fn = async () => fetch('xxxx').josn()
const [err, res] = await to<User[]>(fn())

联合类型和交叉类型

  • 联合类型,即变量可以符合多种约束中的其中一种
typescript
type Test = string | number | false

注意联合类型在函数内部使用,大部分情况需要搭配类型断言使用,即前面说到的类型保护

  • 交叉类型
typescript
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
// }

可辨识联合类型

特征:

  • 多个接口或类型别名实现的联合类型
  • 每个接口或别名都具有一个或多个相同的属性类型
typescript
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
typescript
type Test = {
  a: string
  b: boolean
  c: number
}

type TestKeys = keyof Test
// type TestKeys = 'a' | 'b' | 'c'
  • typeof
typescript
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>

实际使用场景

typescript
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 属性遍历

typescript
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 类型推断,可以理解为定义了一个变量使用常见:例如对于防抖函数的封装:

typescript
type Debounce = (fun: Function) => Function

因为这个函数 fun 的参数是不固定的,所以我们一般有两种方式去获取

  • 使用泛型
  • 使用 Parameter 和 ReturnType(这两个工具都是基于 infer 实现的, 基本上开源的所有第三方工具库的防抖函数都是用这种方式声明的,包括 lodash、underscroe)
typescript
// 泛型实现
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
}

其他场景:

typescript
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[]
typescript
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

javascript
;(() => {
  function setTitle(title) {
    document && document.title !== title && (document.title = title)
  }

  function getTitle() {
    return document && document.title
  }

  let title = getTitle()
})()

test.html 中引入 test.js

html
<script src="./test.js"></script>

test.d.ts

typescript
// declare: 用于声明全局类型,即整个项目都可以使用
declare function setTitle(title: string | number): void
declare function getTitle(): null | string
declare let title: string

ESM / CJS 模块化库, path、http、express 等 test.js

javascript
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

typescript
// 导出模块类型
export = Test

// 声明一个全局模块
declare module Test {
  function setTitle(title: string): void
  function getTitle(): string
}
typescript
export function setTitle(title: string): void
export function getTitle(): string

备注:目前社区 95% 的开源库都有相应的类型声明文件库了,一般以 @types/xxx 如:

  • lodash --> @types/lodash
  • juquery --> @types/juquery

具体的声明文件模板: ts 官网声明文件模板

ts 声明规范

  1. 类型定义规范

ts 社区常用的类型声明一般有两种声明规范

  • 以大写字母 I 开头,一般项目内部,自定义的类型声明会有这种写法,使用 I 开头一般是为了避免出现同名的类
  • 直接首字母大写,一般 @types/xxxx 里面的类型都是这种写法
typescript
type IUser = {
  //...
}

type User = {
  //...
}

class User implement IUser {}  // 这种情况一般就是使用 IUser,避免混淆

const user1: IUser = new User()
const user2: User

使用哪种方式都先,但是一个项目里面尽量使用统一的规范

  1. 文件名规范

类型声明文件的定义目前社区也有两种比较主流的规范

  • src 下定义一个 types 或者 interface 的文件夹
typescript
src - components - componentA - types - global.d.ts - components - componentA.ts
  • 类型声明文件跟随文件
typescript
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 容器)来管理。这样,对象的创建和依赖关系的配置可以在运行时动态地决定,使得代码更加灵活、可扩展和可测试。

有哪些前端框架使用了装饰器

装饰器分类

  • 类装饰器: ClassDecorator
  • 函数装饰器: MethodDecorator
  • 参数装饰器: ParameterDecorator,
  • 属性装饰器: PropertyDecorator

装饰器由于在 js 中任处于提案状态,所以 ts 的装饰器需要手动在 tsconfig.json 中打开一个配置项才能支持

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 这个文件:

typescript
// 类装饰器
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 的规范来看,目前函数装饰器和参数装饰器是可以独立在类外面使用的,但是我自己测试的时候发现总会有各种奇奇怪怪的问题

类装饰器

类装饰器的类型定义

typescript
declare type ClassDecorator = 
  <TFunction extends Function>(target: TFunction) => TFunction | void

可以看出,类装饰器实际就是一个函数,这个函数接受一个参数,这个参实际上就是具体被装饰的类案例:

typescript
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,但是实际上这个类实例化之后的对象拿不到这个新增的属性

这时,就可以利用【同名合并】我们可以解决这个问题,即代码中补充这样一个类型声明

ts
interface Person {
  personName: string
}

比如 ArkTs 的自定义组件的装饰器就是利用这个实现的,如下: ArkTs 官网代码片段

typescript
@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 封装属性样式装饰器
  • ...

属性装饰器

类型定义

typescript
// 属性装饰器
declare type PropertyDecorator = (
  target: Object, // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
  propertyKey: string | symbol 成员的名字
) => void;

注意:

  • 装饰器不能出现在 xx.d.ts 的文件,也不能出现在任何外部上下文中。
  • 属性描述符不会做为参数传入属性装饰器,这与 TypeScript 是如何初始化属性装饰器的有关。因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
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,
  })
}
typescript
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 里面对变量进行了一些额外处理

函数装饰器

typescript
declare type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>,
) => TypedPropertyDescriptor<T> | void

函数装饰器和参数装饰器也都是类似的实现方式,这里就不细讲了,直接放一段 midwayjs 官网代码片段看下就行:

typescript
// 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 默认值,这时就需要使用工厂模式来实现我们需要的装饰器

typescript
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()

组合装饰器

有时候,对与一个变量,我们可以需要多个装饰器对他进行同时修饰,对于使用多个装饰器装饰一个变量(类、函数、方法、属性)的行为,一般被称为组合装饰器

  • 普通装饰器组合

  • 装饰器工厂组合

  • 混合装饰器组合

    组合装饰器执行顺序:

    • 普通装饰器组合:从下往上执行
    • 装饰器工厂组合:先从上往下执行工厂函数,得到全部装饰器,在从下往上执行
    • 混合装饰器组合:先执行工厂装饰器,吧返回的装饰器放在对应位置,然后从下往上执行
  • 普通装饰器组合

普通装饰器组合

  • 装饰器工厂组合

装饰器工厂组合

  • 混合装饰器

混合装饰器

typescript
@setName()
@setAge
@setTitle()
@setCity

-->  下划线假设是返回的装饰器

@_setName
@setAge
@_setTitle
@setCity

ts 的内置工具

typescript
interface Person {
  name: string
  age: number
  sex?: 'man' | 'woman'
}

注意:下面说的几个工具类,定义的方式都可以在 lib.es5.d.ts 这个文件内找到

Record

typescript
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;
// }

实现

typescript
type Record<T extends keyof any, U> = {
  [K in T]: U
}

Readonly

将变量变为只读变量

typescript
type T6 = Readonly<Person>

// type T6 = {
// 	readonly name: string;
// 	readonly age: number;
// 	readonly sex?: "man" | "woman" | undefined;
// }

实现

typescript
type Readonly<T> = {
  // 这里的 in,可以理解为是使用 for in 去实现的
  readonly [K in keyof T]: T[K]
}

readonly 编辑的结果,实际上就是修改对象的 writeable 属性为 false,类型下面的代码:

javascript
Object.defineProprty({}, 'name', {
  value: '李云龙',
  writeable: false,
})

ReadonlyArray

作用:把一个数组变成只读数组,包括数组原型链上的方法和 readonly 的区别, 如下,使用 断言 和 readonly 进行约束之后的数组,虽然无法直接修改,但是还是可以通过原型链上的方法去修改的

typescript
const arr11 = [1, 2] as const
arr11.forEach((item) => {
  item++
})

const arr22: readonly number[] = [1, 2]
arr22.forEach((item) => {
  item++
})

代码比较多,建议看源码,这里只挑一部分源码

typescript
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

作用:创建一个新的类型声明,属性全部变成可选

typescript
type T1 = type T1 = Partial<Person>

// type T1 = {
// 	name?: string | undefined;
// 	age?: number | undefined;
// 	sex?: "man" | "woman" | undefined;
// }

实现

typescript
type Partial<T> = {
  [K in keyof T]?: T[k]
}

Required

作用:创建一个新的类型声明,属性全部变成必选

typescript
type T2 = Required<Person>

// type T2 = {
// 	name: string;
// 	age: number;
// 	sex: 'man' | 'woman';
// }

实现

typescript
type Required<T> = {
  [K in keyof T]-?: T[k]
}

Pick

作用: 提取出指定的类型作为一个新的类型声明

typescript
type T3 = Pick<Person, 'name' | 'age'>

// type T3 = {
// 	name: string;
// 	age: number;
// }

实现

typescript
type Pick<T, U extends keyof T> = {
  [K in U]: T[U]
}

Exclude

TypeScript - 理清 Omit 与 Exclude 的关系与区别 作用:类型过滤

typescript
type T3 = Exclude<string | number, number> // string

实现

typescript
type Exclude<T, U> = T extends U ? never : T

即上面的 T3 可以这么理解

typescript
string extedns number  --->  string
number extends number  --->  never
string & never --> string

备注: T 必须是一个联合类型

Extract

作用:提取,exclude 的反逻辑

typescript
type T5 = Extract<string | number, number> // number
typescript
string extedns number  --->  never
number extends number  --->  number
never & number --->  number

实现:

typescript
type Extract<T, U> = T extends U ? T : never

Omit

作用: 提取出非指定的类型作为一个新的类型声明

typescript
type T4 = Omit<Person, 'name' | 'age'>

// type T4 = {
// 	sex?: "man" | "woman" | undefined;
// }

实现

typescript
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

Omit 和 Exclude 都是排除某几个类型声明,那它们的区别是?

  • Exclude 的接受的第一个泛型必须时联合类型
  • Omit 接受的第一个泛型必须是一个接口或者类型别名

Omit 和 Exclude 区别

Parameters

作用:获取函数的参数类型

typescript
type Parameters<T extends (...args: any) => any> 
  = T extends (...args: infer P) => any ? P : never

ReturnType

作用:获取函数的返回值

typescript
type ReturnType<T extends (...args: any) => any> 
  = T extends (...args: any) => infer R ? R : any

InstanceType

作用:获取类的实例类型

typescript
type InstanceType<T extends abstract new (...args: any) => any> 
  = T extends abstract new (...args: any) => infer R
    ? R
    : any

到这 ts 的语法使用就基本讲完了,剩下的如 命名空间、三斜杠标签等开发过程基本不会用到的知识点可以到官网上查看

ts 开发的一些快速工具

本站中引用到的其他资料,如有侵权,请联系本人删除