浅析 TypeScript 类型推导

前言

在刚接触 TypeScript 时,我仅仅是对变量,函数进行类型标注,主要也就用到了 typeinterface,泛型等内容;后来因为要开发组件库,于是打开官方文档稍微“进修”了一下,了解了一些工具类型例如 PickReturnTypeExclude 等,以及 tsconfig 的一些编译配置,总的来说也是浅尝辄止。
直到最近开发地图组件库时,产生了一些奇怪的需求,比如:已知有事件 ['click', 'touch', 'close'] ,如何根据这个数组生成一个类型,其属性为 onClickonTouchonClose ,向同事请教后未果,于是决定深入学习下 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 这个关键字可以延伸一下:JavaScripttypeof 可以帮助 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 类型

基本类型

基本类型,也可以理解为原子类型。包括 numberbooleanstringnullundefinedfunctionarraysymbol 字面量(truefalse1"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

因为 tuplereadonly 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,而不能变成其他的类型~遍历对象主要通过 inkeyof 操作符实现。

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
当我们读完并理解上述内容后,应该可以轻松完成类型体操的简单题和中等难度的题~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容