【问题标题】:Is it possible to infer generic types recursively in TypeScript?是否可以在 TypeScript 中递归地推断泛型类型?
【发布时间】:2020-06-25 10:48:54
【问题描述】:

以这段代码为例:

const [divEl, spanEl] = createElements([
    ['div', { id: 'test' }, [
        ['a', { href: 'test' }, [
            ['img', { src: 'test' }, null]
        ]],
        ['img', { href: 'test' }, null]
    ]],
    ['span', null, null]
]);

我希望 TypeScript 推断 divEl 的类型为 HTMLDivElementspanEl 的类型为 HTMLSpanElement。而且我还希望它检查给定的属性并根据推断的类型显示错误(例如,它应该在['img', { href: 'test' }, null] 上显示错误,因为HTMLImageElement 没有href 属性)。

经过一番研究,这是我目前所拥有的:

type ElementTag = keyof HTMLElementTagNameMap;

type ElementAttributes<T extends ElementTag> = {
    [K in keyof HTMLElementTagNameMap[T]]?: Partial<HTMLElementTagNameMap[T][K]> | null;
};

type ElementArray<T extends ElementTag> = [
    T,
    ElementAttributes<T> | null,
    ElementArray<ElementTag>[] | string | null
];

type MappedElementArray<T> = {
    [K in keyof T]: T[K] extends ElementArray<infer L> ? HTMLElementTagNameMap[L] : never;
};

// This is the signature for the createElements function.
type CreateElements = <T extends ElementArray<ElementTag>[] | []>(
    array: T
) => MappedElementArray<T>;

我必须将 ElementArray&lt;ElementTag&gt;[] 更改为通用名称,但我不确定如何继续。

这可能吗?

我知道可以手动完成,但 T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, ... 并不漂亮。

【问题讨论】:

  • 这听起来可能需要 Variadric Tuples 在 Typescript 4.0 中发布。
  • 有趣。我还没有想出如何将它应用于这个问题,但是谢谢,我会花一些时间来解决这个问题。

标签: typescript


【解决方案1】:

(部分回答)

TypeScript 不喜欢递归类型。只要您不需要推理,您基本上可以解决这个问题……但是,由于这里需要推理,我认为不可能让 TS 迈出最后一步。

您可以拥有以下功能之一:

  1. 将返回类型推断为具有正确返回类型的元组。
  2. 对传递的数组进行深度类型检查

第一个很简单。我们只需要createElements 中的T 来扩展一个有区别的联合。你已经有了这个,但这是一个稍微不同的看待它的方式。

type DiscriminatedElements = {
    // you can, of course, do better than unknown here.
    [K in keyof HTMLElementTagNameMap]: readonly [K, Partial<HTMLElementTagNameMap[K]> | null, unknown]
}[keyof HTMLElementTagNameMap]

type ToElementTypes<T extends readonly DiscriminatedElements[]> = {
    [K in keyof T]: T[K] extends [keyof HTMLElementTagNameMap, any, any] ? HTMLElementTagNameMap[T[K][0]] : never
}

declare const createElements: <T extends readonly DiscriminatedElements[] | []>(
    array: T
) => ToElementTypes<T>

createElements([
    ['span', {}, null]
]) // [HTMLSpanElement]

第二个有点棘手。正如我之前所说,TS 不喜欢递归类型。见GH#26980。也就是说,我们可以绕过它来创建一个任意深度的类型进行检查……但是如果我们尝试将这种类型与任何推理结合起来,TS 将意识到它可能是无限的。

type DiscriminatedElements = {
    [K in keyof HTMLElementTagNameMap]: readonly [K, Partial<HTMLElementTagNameMap[K]> | null]
}[keyof HTMLElementTagNameMap]

type NumberLine = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
type CreateElementArray<T extends readonly [any, any], N extends number> = T extends readonly [infer A, infer B] ? {
    done: readonly [A, B, null],
    recurse: readonly [A, B, null | readonly CreateElementArray<DiscriminatedElements, NumberLine[N]>[]]
}[N extends 0 ? 'done' : 'recurse'] : never

// Increase up N and NumberLine as required
type ElementItem = CreateElementArray<DiscriminatedElements, 4>

declare const createElements: (
    array: readonly ElementItem[]
) => HTMLElement[];


const [divEl, spanEl] = createElements([
    ['div', { id: 'test' }, [
        ['a', { href: 'test' }, [
            ['img', { src: 'test' }, null]
        ]],
        ['img', { href: 'test' }, null] // error, as expected
    ]],
    ['span', null, null]
]);

我认为可变参数元组在这里对您没有帮助。它们将是该语言的绝佳补充,但不能解决您在此处尝试建模的问题。

让您保持两全其美的解决方案是接受 HTML 元素作为元组中的第三项,并在该数组中简单地调用 createElements

【讨论】:

  • 第二个很棒,谢谢!我想我可以从中受益更多,因为我想我可以通过 as [HTMLDivElement, HTMLSpanElement] 获得正确的返回类型。我曾想过在数组中调用createElements,但我更喜欢简单的数组结构。
猜你喜欢
  • 1970-01-01
  • 2019-09-17
  • 1970-01-01
  • 2020-07-23
  • 2017-10-04
  • 2020-01-27
  • 1970-01-01
  • 2020-09-09
  • 2018-01-18
相关资源
最近更新 更多