[ TS 类型体操 ] 初体验之 infer

1,082 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 3 天,点击查看活动详情

前言

在上一篇文章中,我们体会到了 infer 在类型体操中的重要作用。本文将着重介绍 infer 在各种情况下的使用技巧,如果有遗漏的地方,欢迎在评论区中指出。

infer 在数组中使用

在上一篇文章中我们已经了解到了 infer 在数组中的使用,我们再通过几道题来加深一下印象

First of Array 与 Last of Array

在这两道题中,我们将实现从一个数组中取出第一个值与最后一个值。

// First of Array
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

// Last of Array
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type tail1 = Last<arr1> // expected to be 'c'
type tail2 = Last<arr2> // expected to be 1

在数组中我们可以利用 infer 与 Rest Parameter 来很容易的得到答案。

在 First of Array 中,我们可以将 Rest 参数放在后面,只需要提取出 First 即可。

type First<T extends any[]> = T extends [infer first, ...infer rest] ? first : never

同理,在 Last of Array 中,我们将 Rest 参数放在前面,取出后面的 Last 参数即可

type Last<T extends any[]> = T extends [...infer Rest, infer Last] ? Last : never

以上就是 infer 在数组中的初步使用,主要思想就是利用 infer 来推断希望得到的元素的位置最终来实现想要的效果。

infer 在泛型中使用

infer 同样可以在泛型中使用,可以推断出泛型传入的类型,来达到我们的目的,他的用法类似于 T extends type<infer T> ? T : never

Awaited

题目描述:如果我们有一种像 Promise 一样包裹了类型的类型,我们该怎样拿到包裹在这种类型中的类型。例如,我们有一个 Promise<ExampleType>,该怎么拿到 Exampletype

type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string

测试用例如下

import type { Equal, Expect } from '@type-challenges/utils'

type X = Promise<string>
type Y = Promise<{ field: number }>
type Z = Promise<Promise<string | number>>
type Z1 = Promise<Promise<Promise<string | boolean>>>

type cases = [
  Expect<Equal<MyAwaited<X>, string>>,
  Expect<Equal<MyAwaited<Y>, { field: number }>>,
  Expect<Equal<MyAwaited<Z>, string | number>>,
  Expect<Equal<MyAwaited<Z1>, string | boolean>>,
]

// @ts-expect-error
type error = MyAwaited<number>

通过分析测试用例,我们可以得到如下信息

  1. 希望传入的类型不能是非 Promise 类型
  2. 当传入 Promise 包裹的类型,会返回被包裹的类型,
  3. 支持嵌套传入

有了上面的基本用法,我们可以很容易的写出上面几点

对传入的非 Promise 值进行判断

type MyAwaited<T extends Promise<unknown>>

在上面代码中我们使用 unknown 来指代 Promise 传入的值,而不是使用 any,当然使用 any 也是可以的,但使用 any 意味着我们不再对 Promise 进行类型检查。而使用 unknown,TS 还对传入的类型进行检查。借用官网的例子

function f1(a: any) {

a.b(); // OK

}

function f2(a: unknown) {

a.b(); //error: Object is of type 'unknown'

}

在使用 any 时,TS 彻底放弃了类型检查,而使用 unknown,TS 对类型的检查还在生效,所以在不知道具体类型时,更推荐使用 unkonwn 来代替

返回被包裹的类型

type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer X> ? X : never;

只需使用 infer 即可得到被包裹的类型,使用 extends 来判断 T 是否是 Promise<T> 类型。如果 T 传入的格式是 Promise<T> 时则返回 X,如果不是,则返回 never。但我们已经对传入的类型进行限制,所以永远不可能返回 never

支持嵌套传入

只需对推断出的 X 进行判断是否是 Promse 如果是则递归调用,如果不是则返回 X,即可实现这个功能

type MyAwaited<T extends Promise<unknown>> = T extends Promise<infer X>
  ? X extends Promise<unknown>
    ? MyAwaited<X>
    : X
  : never;

infer 在函数中使用

infer 同样可以在函数中使用,用来推断出函数传入参数的类型,以及函数返回值的类型

推断参数 Parameters

在这道题中,我们实现一个内置泛型 Parameters,它的作用是返回一个函数传入的参数类型

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]

测试用例

const foo = (arg1: string, arg2: number): void => {}
const bar = (arg1: boolean, arg2: { a: 'A' }): void => {}
const baz = (): void => {}

type cases = [
  Expect<Equal<MyParameters<typeof foo>, [string, number]>>,
  Expect<Equal<MyParameters<typeof bar>, [boolean, { a: 'A' }]>>,
  Expect<Equal<MyParameters<typeof baz>, []>>,
]

在对传入参数进行限制时,我们可以使用类似于 (...args: number[]) => any 来对传入的参数进行声明。所以我们可以利用这一特点使用 infer 来对传入的参数以及类型进行推断。

(...args: infer X) => any,我们就可以对传入的函数类型进行判断

type MyParameters<T extends (...args: any[]) => unknown> = T extends (...args: infer P) => unknown ? P : never;

推断返回值 Get Return Type

我们可以通过利用 T extends (...args: any[]) => infer U 的格式来推断一个函数返回值的类型。在 type-challenges 中有相应的案例,在此不再重复叙述,给出问题链接

值得注意的是,infer 并不可以在参数中通过 extends 使用,以下例子是 TS 所不允许的

type ReturnType<T extends (...args: any[]) => infer R> = R; // Error, not supported

infer 在字符串中使用

infer 同样可以推断字符串,也就是字面量类型,他的形式如下

T extends `${someWords}${infer X}`

Replace 与 Replace All

Replace

在本题中,实现一个 Replace<S, From, To>,该泛型可以把 S 字符串中的字符串 From 替换为 To

type replaced = Replace<'types are fun!', 'fun', 'awesome'> // expected to be 'types are awesome!'

测试用例

type cases = [
  Expect<Equal<Replace<'foobar', 'bar', 'foo'>, 'foofoo'>>,
  Expect<Equal<Replace<'foobarbar', 'bar', 'foo'>, 'foofoobar'>>,
  Expect<Equal<Replace<'foobarbar', '', 'foo'>, 'foobarbar'>>,
  Expect<Equal<Replace<'foobarbar', 'bar', ''>, 'foobar'>>,
  Expect<Equal<Replace<'foobarbar', 'bra', 'foo'>, 'foobarbar'>>,
  Expect<Equal<Replace<'', '', ''>, ''>>,
]

我们可以通过形如以下的判断条件很容易的提取出 From,我们只需在后面返回时,将 From 替换为 To 就可以实现该题目

T extends `${infer Front}${From}${infer Rest}`

我们来直接看解答

type Replace<S extends string, From extends string, To extends string> = From extends ""
  ? S
  : S extends `${infer Front}${From}${infer Rest}`
  ? `${Front}${To}${Rest}`
  : S;

值得注意的是,根据测试用例,当 To 是一个空值时,直接返回字符串即可,所以在进行判断时,先对 To 进行判断。之后再进行下一步判断。在使用 infer 对字符串进行判断时,会自动匹配满足含有 From,并将满足 ${infer Front}${From}${infer Last} 格式的 Front 与 Rest 两个字串返回。 对于 Expect<Equal<Replace<'foobar', 'bar', 'foo'>, 'foofoo'>> 这个测试案例,Front 与 Rest 的值分别会是 'foo''',所以我们在返回字符串时只需将 Front 与 Rest 和 To 按照希望位置进行拼接即可达到预期效果。

Replace All

在本题中,我们对 Replace 进行升级,我们将对所有符合 From 的字串进行替换,最终返回目标值。

type replaced = ReplaceAll<'t y p e s', ' ', ''> // expected to be 'types'

测试用例如下

type cases = [
  Expect<Equal<ReplaceAll<'foobar', 'bar', 'foo'>, 'foofoo'>>,
  Expect<Equal<ReplaceAll<'foobar', 'bag', 'foo'>, 'foobar'>>,
  Expect<Equal<ReplaceAll<'foobarbar', 'bar', 'foo'>, 'foofoofoo'>>,
  Expect<Equal<ReplaceAll<'t y p e s', ' ', ''>, 'types'>>,
  Expect<Equal<ReplaceAll<'foobarbar', '', 'foo'>, 'foobarbar'>>,
  Expect<Equal<ReplaceAll<'barfoo', 'bar', 'foo'>, 'foofoo'>>,
  Expect<Equal<ReplaceAll<'foobarfoobar', 'ob', 'b'>, 'fobarfobar'>>,
  Expect<Equal<ReplaceAll<'foboorfoboar', 'bo', 'b'>, 'foborfobar'>>,
  Expect<Equal<ReplaceAll<'', '', ''>, ''>>,
]

根据上一题我们可知,只需对 Rest 字串继续进行处理即可得到答案。

type ReplaceAll<S extends string, From extends string, To extends string> = From extends ""
  ? S
  : S extends `${infer First}${From}${infer Rest}`
  ? `${First}${To}${ReplaceAll<Rest, From, To>}`
  : S;

我们只需在判断完一处位置时,将未处理的 Rest 字串传入到 ReplaceAll 作为新的字串进行递归处理即可得到答案。


这是我自己的练题仓库,里面会总结我在练习类型体操中遇到的相关问题以及相关知识点,如果这对你有帮助的话就给我一个 star 吧😄