前言
在刚接触 TypeScript
时,我仅仅是对变量,函数进行类型标注,主要也就用到了 type
、interface
,泛型等内容;后来因为要开发组件库,于是打开官方文档稍微“进修”了一下,了解了一些工具类型例如 Pick
、ReturnType
,Exclude
等,以及 tsconfig
的一些编译配置,总的来说也是浅尝辄止。
直到最近开发地图组件库时,产生了一些奇怪的需求,比如:已知有事件 ['click', 'touch', 'close']
,如何根据这个数组生成一个类型,其属性为 onClick
、onTouch
,onClose
,向同事请教后未果,于是决定深入学习下 TypeScript
。经过一番学习,实现了一个版本如下:
type EventToHandler<A extends readonly string[] , H> = {
[K in A[number] as `on${Capitalize<K>}`]: H
}
const event = ['click', 'touch', 'close'] as const;
type EventMap = EventToHandler<typeof event, (e: any) => void>
测试结果
下面,我将分享对学习内容的总结~
一、操作符
keyof
The keyof operator takes an object type and produces a string or numeric literal union of its keys.
keyof 操作符接受一个对象类型,并产生一个字符串或其键的数字字面值联合类型。参考:https://www.typescriptlang.org/docs/handbook/2/keyof-types.html
interface Object {
p: string
q: number
}
type Key = keyof Object // 'p' | 'q'
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number
typeof
JavaScript already has a typeof operator you can use in an expression context, TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property.
JavaScript已经有了一个 typeof 操作符,你可以在表达式上下文中使用,TypeScript添加了一个 typeof 操作符,你可以在类型上下文中使用它来引用变量或属性的类型。参考:https://www.typescriptlang.org/docs/handbook/2/typeof-types.html
// Javascript
typeof null === 'object' // true
// TypeScript
type A = typeof null // any
type B = typeof '1' // '1'
const obj = {
p: 1,
q: '1'
}
type Object = typeof obj // { p: number, q: string }
typeof
这个关键字可以延伸一下:JavaScript
中 typeof
可以帮助 TypeScript
实现类型收紧。
除此之外 instanceof
以及 TypeScript
中的 is
也有相同的作用。类型收紧在函数重载中很有用~
declare function isString(str: unknown): str is string
declare function isNumber(str: unknown): str is number
function main(str: number): number
function main(str: string): string
function main(str) {
if (isString(str)) {
// (parameter) str: string
return String(str);
}
if (isNumber(str)) {
// (parameter) str: number
return Number(str);
}
throw new Error('unExpected param type');
}
in
抱歉在官方文档上没有找到 in
操作符相关的解释,我只能从实践的角度总结它的作用:TypeScript
中的 in
在对象映射操作上起着至关重要的作用~下文会介绍其具体作用。
infer
TypeScript
中的 infer
用于在泛型类型中推断出其某个参数的类型。通常情况下,我们可以将泛型类型传递给一个具体类型来获取它的类型,但有时候需要从泛型类型中推断出某个输入类型或输出类型,这时候就可以使用 infer
来实现。
注意:infer
只能用在 extends
之后。
type MyAwaited<P extends Promise<unknown>> = P extends Promise<infer T> ? T : never;
type Test = MyAwaited<Promise<string>> // string
type RetrunType<T> = T extends (...args: any[]) => infer U ? U : never
二、类型基础
2.1 类型
基本类型
基本类型,也可以理解为原子类型。包括 number
、boolean
、string
、null
、undefined
、function
、array
、symbol
字面量(true
,false
,1
,"a"
)等,它们无法再细分。
复合类型
复合类型可以分为三类:
-
union
,指一个无序的、无重复元素的集合。 -
tuple
,可简单看做一个只读数组的类型。 -
map
,和JavaScript
中的对象一样,是一些没有重复键的键值对。
type union = '1' | '2' | true | symbol
const tuple = [1, 2, 3] as const;
type Tuple = typeof tuple;
interface Map {
name: string
age: number
}
2.2 取值方式
union
TypeScript
官方没有提供 union
的取值方式,这也直接导致了和 union
相关的类型变换变得比较复杂。
tuple
因为 tuple
是 readonly Array<any>
类型,所以 tuple
也可以像数组一样使用数字进行索引。
const tuple = [1, 2, 3, '1'] as const;
type Tuple = typeof tuple;
type T0 = Tuple[0] // 1
type T3 = Tuple[3] // '1'
type T4 = Tuple[4] //
type Union = Tuple[number] // 1 | 2 | 3 | '1'
map
map
取值和 JavaScript
中对象取值的方式一致
interface Object {
p: string
q: number
}
type A = Object['p'] // string
type B = Object[keyof Object] // string | number
2.3 遍历方式
在 TypeScript
的类型系统中无法使用循环语句,所以我们只能用递归来实现遍历,能参与逻辑判断的操作符只有 extends
和 三元运算符 ? ... : ...
。
union
union
的遍历最简单,只需要用 extends
即可完成。
type Exclude<T, U> = T extends U ? never : T
type A = Exclude<'1' | '2', '2'> // '1'
tuple
元组遍历主要通过 infer
和扩展运算符 ...
实现,通过检查 rest
参数是否为空数组来判断是否递归到最后一项。
export type Join<
A extends readonly string[],
S extends string,
P extends string = ''
> = A extends readonly [infer F extends string, ...infer R extends readonly string[]]
? R extends [] // F tuple 的最后一个元素
? `${P}${F}`
: Join<R, S, `${P}${F}${S}`>
: P
declare function join<A extends readonly string[], S extends string>(array: A, s: S): Join<A, S>
const arr = ['hello', 'world'] as const
const str = join(arr, ' ') // 'hello world'
type Str = Join<typeof arr, ' '> // 'hello world'
字面量数组
字符串的遍历方式和数组类似,也通过 infer
实现,另外还需要模板字符串辅助。
export type Split<
S extends string,
P extends string,
A extends string[] = []
> = S extends `${infer F}${infer R}` ?
R extends '' // F 已经是最后一个字符
? F extends P
? A
: [...A, F] // F 是一个非分隔符的字符
: F extends P // F 不是最后一个字符
? Split<R, P, A> // F 是分隔符,那么丢弃
: Split<R, P, [...A, F]> // F 不是分隔符,
: string[]
declare function split<S extends string, P extends string>(str: S, p: P): Split<S, P>
const arr = split('1,2,3', ',') // ["1", "2", "3"]
map
严格来讲,遍历对象不能称之为“遍历”,而是“映射”,因为一个 map
只能映射成另外一个 map
,而不能变成其他的类型~遍历对象主要通过 in
和 keyof
操作符实现。
type Required<T> = {
[K in keyof T]-?: T[K]
}
type Partial<T> = {
[K in keyof T]+?: T[K]
}
type ReadonlyAndRequired<T> = {
+readonly[K in keyof T]-?: T[K]
}
interface PartialObj {
p?: string
}
type RP = Required<PartialObj> // {p: string}
type RRP = ReadonlyAndRequired<PartialObj> // { readonly p: string}
三、类型变换
3.1 union
union to map
type SetToMap<S extends number | symbol | string, F> = {
[K in S]: F
}
type union = '1' | '2'
type Map = SetToMap<union, number>
union to tuple
// ref: https://github.com/type-challenges/type-challenges/issues/737
1 | 2 => [1, 2]
/**
* UnionToIntersection<{ foo: string } | { bar: string }> =
* { foo: string } & { bar: string }.
*/
type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => 0 : never
) extends (arg: infer I) => 0
? I
: never;
/**
* LastInUnion<1 | 2> = 2.
*/
type LastInUnion<U> = UnionToIntersection<U extends unknown ? (x: U) => 0 : never> extends (x: infer L) => 0
? L
: never;
/**
* UnionToTuple<1 | 2> = [1, 2].
*/
type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never]
? []
: [...UnionToTuple<Exclude<U, Last>>, Last];
3.2 tuple
tuple to map
type TupleToMap<T extends readonly any[], P> = {
[K in T[number]]: P
}
const a = [1, 2] as const
type Tuple = typeof a
type union = Tuple[number] // 1 | 2
tuple to union
type TupleToUnion<A extends readonly any[], U = never> = A extends readonly [infer F, ...infer R]
? R extends []
? U | F
: TupleToUnion<R, U | F>
: never
[1,2] => 1 | 2
3.3 map
map to union
type MapToUnion<M> = keyof M
map to tuple
与 union to tuple
一致
四、类型体操
类型体操:type-challenges
当我们读完并理解上述内容后,应该可以轻松完成类型体操的简单题和中等难度的题~
网友评论