TS 高级类型
TS,全称 TypeScript,是 JavaScript 的超集。
它在 JavaScript 的基础上引入了静态类型系统。通过在代码中标注类型,可以帮助开发者在编译阶段就可以发现错误,提高代码可读可维护性。
并且类型检查仅在编译时进行,在运行时,类型会被擦除,也就是说,TS 并不会引入额外的运行时开销。
由于 TS 完全兼容 JS,也就是说,所有合法的 JS 代码在 TS 中都是合法的。因此,如果你已经掌握了 JS,那么学习 TS 就如同在已有基础上增加类型支持,门槛并不高。
类型系统的好处
我们都知道,JavaScript 是一门动态类型语言,变量的类型可以在运行时随时改变,这赋予了它极大的灵活性。
但这种灵活性也带来了不小的隐患。例如,某个函数期望接收两个数字作为参数,但调用时却传入了字符串,由于没有类型约束,这种错误只能在运行时才会暴露出来:
function add(a, b) {
return a + b
}
add(1, '2') // 结果为 '12',不是预期中的 3
为了避免这类错误,TypeScript 引入了静态类型系统,通过在代码中标注类型,使得可以在开发阶段就发现潜在问题,提升代码的健壮性与可维护性。
function add(a: number, b: number): number {
return a + b
}
在上面的例子中,如果传入非 number 类型的参数,TS 会立刻报错。
需要注意,TS 的类型系统只在编译时生效,不会引入任何运行时代码或开销。这意味着,类型信息在编译之后会被擦除,不会影响运行时性能。
TS 采用的是结构化类型系统,也称为“鸭子类型”。
如果一个东西看起来像鸭子,叫起来像鸭子,走起来也像鸭子,那么它就是鸭子。
换句话说,只要一个对象的结构符合某个类型的定义,就可以被视为该类型,无需显式声明。
type Point = { x: number; y: number }
const p1: Point = { x: 10, y: 20 } // 合法
const obj = { x: 5, y: 15, z: 25 }
const p2: Point = obj // 合法,因为结构匹配
这种灵活性让 TS 既具备类型系统的安全性,也保留了 JS 的表达能力。
类型系统的使用
在 TS 中,提供了关键字 type
和 interface
用于分别定义类型和接口。
以下是简单的例子:
type UserID = number
type UserInfo = {
id: UserID
name: string
age: number
}
type UserInfoHandle = (info: UserInfo) => UserInfo
interface UserInfoInterface {
id: UserID
name: string
age: number
}
interface UserInfoInterfaceHandle {
(info: UserInfoInterface): UserInfoInterface
}
const userInfo: UserInfo = {
id: 1,
name: '1',
age: 1
}
const userInfoInterfaceHandle: UserInfoInterfaceHandle = info => info
const userInfoInterface: UserInfoInterface = userInfo
const userInfoHandle: UserInfoHandle = userInfoInterfaceHandle
以上例子分别使用 type
和 interface
关键字定义了用户的数据类型结构。
泛型
泛型是类型系统中不可或缺的角色。
它允许我们编写可以适用于多种类型的代码,而不必重复编写相同的逻辑。
泛型通过类型参数来实现,这些参数在定义时并不指定具体类型,而是在使用时由调用者指定。
// 非泛型写法:需为不同类型编写重复逻辑
function identityNumber(arg: any, defaultVal: number): number { return arg ?? defaultVal }
function identityString(arg: any, defaultVal: string): string { return arg ?? defaultVal }
// 泛型写法:通过类型参数T实现类型抽象
function identity<T>(arg: any, defaultVal: T): T {
return arg ?? defaultVal
}
// 使用时动态指定类型
const num = identity<number>(100, 100) // number类型
const str = identity<string>('hello', 'hello') // string类型
// 类型推论场景(TS自动推导类型)
const bool = identity(true, true) // boolean类型
泛型可以广泛应用在类型系统中。
interface Data<T> {
value: T
}
type Route<Req, Res> = {
request: Req
response: Res
}
function Clone<T>(data: T): T {
return JSON.parse(JSON.stringify(data))
}
class Item<T> {
constructor(
public readonly value: T
) {}
}
class ExtItem<T> extends Item<T> implements Data<T> {
constructor(
public readonly value: T
) {
super(value)
}
}
const route: Route<'req', 'res'> = { request: 'req', response: 'res' }
const route_copy = Clone(route)
const item1 = new Item<typeof route>(route)
const item2 = new ExtItem<boolean>(false)
接口和类型
通常来说,接口用于定义一组方法,类型用来定义一些字段集合。
但在 TS 中,并没有明确限制。接口可以定义字段集合,类型也可以定义一组方法。
interface IFather {
type: string
eat(food: string): void
}
interface IChild extends IFather {
name: 'child'
drink(water: string): void
}
type TFather = {
type: string
eat(food: string): void
}
type TChid = {
name: 'child'
drink(water: string): void
} & TFather
const f1: IFather = {
type: 'father',
eat(food) {
console.log("eat", food)
}
}
const c1: IChild = {
name: 'child',
type: 'child',
eat(food) {
console.log("eat", food)
},
drink(water) {
console.log("drink", water)
},
}
class Father implements IFather{
constructor(
public readonly type: string
) {}
eat(food: string): void {
console.log("eat", food)
}
}
const f2: TFather = f1
const c2: TChid = c1
类型约束
很多时候,我们希望类型可以更加具体,而不是广泛。
function CloneObject<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj))
}
以 CloneObject
为例,我们可能只希望克隆对象,但是实际上可以 CloneObject(1)
, CloneObject('hello')
克隆基本类型。
function CloneObj<T extends Record<string, string>>(obj: T): T {
return JSON.parse(JSON.stringify(obj))
}
T extends Record<string, string>
将泛型 T
约束为匹配 Record<string, string>
类型。
类型体操
类型体操,是对于 TS 的类型操作的一种调侃。
在 TS 中,存在有多种内置的类型工具。
有将对象属性全部转换为可选的 Partial
和全部转为必须的 Required
。
从对象中取出指定键集合的 Pick
,忽略指定键的 Omit
,乃至于将字符串转换大小写的 Uppercase
, Lowercase
, Capitalize
, Uncapitalize
等等。
通过类型体操,可以实现复杂类型的定义。
但是正常业务开发中是不会有太复杂的类型体操的。它的应用场景通常是库和框架开发。
交叉类型
用来将类型组合,而不是重复代码。
type a = { a: 1 }
type b = { b: 2 }
type c = a & b
const v: c = { a: 1, b: 2 }
联合类型
限制类型在指定类型集合中。
type a = 1
type b = 2
type c = a | b
const va: c = 1
const vb: c = 2
类型保护
将类型从未知变成已知的。
function isString(data: unknown): data is string {
if (typeof data === 'string') {
return true
}
return false
}
索引类型
允许为类型设置索引。
type List = {
[index: number]: number
}
type obj = {
[key: string]: 'a' | 'b' | 'c'
}
const list = [1, 2, 3] as const
const l: List = list
const el: typeof list[number] = 1
const o: obj = {
'a': 'a',
'b': 'b',
'c': 'c'
}
const val: obj[keyof obj] = 'b'
const len: typeof list['length'] = 3
关于索引类型有一个 TS 初学者容易出现的问题。
type Json = {
[x: string]: string
}
通常这个要表示 JSON 是使用 Record
这个 TS 内置的类型工具的。
以上示例等价于 Record<string, string>
。
类型映射
通过映射类型索引来重新定义类型。
// 内置工具类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 自定义映射类型
type Nullable<T> = {
[P in keyof T]: T[P] | null
};
条件类型
type IsString<T> = T extends string ? T : never
如果传入的 T
匹配 string
那么匹配返回 T
,否则返回 never
。
模板字面量
type Url = `${'http'|'https'}://${string}.com`
type Url = `${'http' | 'https'}://${string}.com`
const google: Url = 'https://google.com'
const example: Url = 'http://example.com'
要求字符串匹配定义格式。
模板字面量的类型推断
type TransformBool<S extends string> = S extends `${string}=${infer b}`
? b extends 'true'
? true
: b extends 'false'
? false
: never
: never
const f: TransformBool<'a=false'> = false
const t: TransformBool<'b=true'> = true
从模板字面量中提取指定内容,随后使用条件类型。
示例
限制数字范围
/** @description (0, N] */
type Enumerate<N extends number, A extends number[] = []> = A['length'] extends N ? A[number] : Enumerate<N, [...A, A['length']]>
/** @description (S, E] */
type IntRange<S extends number, E extends number> = Exclude<Enumerate<E>, Enumerate<S>>
const fromZeroToTen: Enumerate<11> = 1
const inFiveToTen: IntRange<5, 11> = 5
切割字符串为数组
type Split<P extends string, S extends string, Strict = true> = P extends `${infer p}${S}${infer component}`
? Strict extends true
? component extends ''
? [p]
: [p, ...Split<component, S, Strict>]
: [p, ...Split<component, S, Strict>]
: [P]
const a: Split<'a-b-', '-', false> = ['a', 'b', '']
const b: Split<'a-b-', '-', true> = ['a', 'b']
限制字符串开头和结尾
type StartsWith<S extends string, M extends string> = S extends `${M}${string}` ? true : false
type EndsWith<S extends string, M extends string> = S extends `${string}${M}` ? true : false
const str = 'aaabbbccc'
const start: StartsWith<typeof str, 'aaa'> = true
const end: EndsWith<typeof str, 'ccc'> = true
解析路由
type AllPath<P extends string> = P extends `${infer p}/${infer component}` ? Record<p, string> & AllPath<component> : Record<P, string>
type FilterParamPath<T extends Record<string, string>, K extends keyof T = keyof T> = K extends `:${infer p}`
? p extends `${infer param}?${string}` ? param : p
: never
type ParamPath<AllPath extends Record<string, string>> = {
[P in FilterParamPath<AllPath>]: string
}
const p = '/v1/:perfix/major/minor/:patch/:username/publish/:id?tag=1&tag=2&tag=3&category=5&mode=dart&publish=false&list=1,2,3,4,5'
const param: ParamPath<AllPath<typeof p>> = {
id: '',
patch: '',
perfix: '',
username: ''
}
解析值
type ParseList<A extends unknown[], Strict = true> = A[number] extends string ? ParseValue<A[number], Strict> : A[number]
type ParseValue<V extends string, Strict = true> =
V extends 'true' ? Strict extends true ? true : boolean :
V extends 'false' ? Strict extends true ? false : boolean :
V extends `${infer N extends number}` ? Strict extends true ? N : number :
V extends `null` ? null :
V extends `undefined` ? undefined :
V extends `${string},${string}` ? ParseList<Split<V, ','>, Strict>[] :
V
const n: ParseValue<'123'> = 123
const f: ParseValue<'false'> = false
const b: ParseValue<'true'> = true
const list: ParseValue<'a,b,c,d,e'> = ['a', 'b', 'c', 'd', 'e']
const elements: ParseList<typeof list> = 'a'
解析路由参数
type QueryUnit<P extends string> = P extends `${infer k}=${infer v}` ? Record<k, ParseValue<v>> : never
type MergeQuery<A extends Record<string, unknown>, B extends Record<string, unknown>> = {
[K in (keyof A | keyof B)]:
K extends keyof A
? K extends keyof B
? [A[K], B[K]]
: A[K]
: K extends keyof B
? B[K]
: never
}
type QueryHandle<P extends string> = P extends `${infer q}&${infer querys}` ? MergeQuery<QueryUnit<q>, QueryHandle<querys>> : P extends `${infer q}&` ? QueryUnit<q> : QueryUnit<P>
type FlatListEl<A extends unknown> = A extends Array<infer els>
? els extends unknown[]
? FlatListEl<els>
: els
: A
type FlatList<A extends unknown, Els = FlatListEl<A>> = [Els] extends [A] ? A : Els[]
type Query<P extends string> = P extends `${string}?${infer query}` ? QueryHandle<query> : never
type Querys<P extends string, O extends Record<string, unknown> = Query<P>> = {
[K in keyof O]: FlatList<O[K]>
}
const p = '/v1/:perfix/major/minor/:patch/:username/publish/:id?tag=1&tag=2&tag=3&category=5&mode=dart&publish=false&list=1,2,3,4,5'
const query: Querys<typeof p> = {
'tag': [1, 2],
'category': 5,
'mode': 'dart',
publish: false,
list: [1, 2, 3, 4, 5]
}