简体   繁体   English

是否可以在 TypeScript 中递归地推断泛型类型?

[英]Is it possible to infer generic types recursively in TypeScript?

Take this code, for example:以这段代码为例:

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

I want TypeScript to infer the type of divEl to be HTMLDivElement and the type of spanEl to be HTMLSpanElement .我希望 TypeScript 将 divEl 的类型推断为divEl ,将HTMLDivElement的类型spanElHTMLSpanElement And I also want it to check the given attributes and show errors based on the inferred type (for example, it should show an error on ['img', { href: 'test' }, null] , because HTMLImageElement doesn't have an href property).而且我还希望它检查给定的属性并根据推断的类型显示错误(例如,它应该在['img', { href: 'test' }, null]上显示错误,因为HTMLImageElement没有href属性)。

After some research, this is what I have so far:经过一番研究,这是我目前所拥有的:

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>;

I'd have to change ElementArray<ElementTag>[] to something generic, but I'm not sure how to proceed.我必须将ElementArray<ElementTag>[]更改为通用的,但我不确定如何继续。

Is this even possible?这甚至可能吗?

I know it's possible to do it manually, but T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, ... isn't pretty.我知道可以手动完成,但T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, ...并不漂亮。

(Partial answer) (部分回答)

TypeScript doesn't like recursive types. TypeScript 不喜欢递归类型。 You can mostly work around this, so long as you don't require inference... however, since that's required here, I don't think it is possible to get TS to take the last step.只要您不需要推理,您几乎可以解决这个问题……但是,由于这里需要推理,我认为不可能让 TS 迈出最后一步。

You can have one of the following features:您可以拥有以下功能之一:

  1. Infer the return type to be a tuple with the correct return types.将返回类型推断为具有正确返回类型的元组。
  2. Deeply typecheck the passed array对传递的数组进行深度类型检查

The first is easy.第一个很容易。 We just need T in createElements to extend a discriminated union.我们只需要createElements中的T来扩展可区分的联合。 You had this already, but this is a slightly different way of looking at it.你已经有了这个,但这是一种稍微不同的看待它的方式。

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]

The second is a bit trickier.第二个有点棘手。 As I said before, TS doesn't like recursive types.正如我之前所说,TS 不喜欢递归类型。 See GH#26980 .参见GH#26980 That said, we can work around it to create a type of an arbitrary depth for checking... but if we try to combine this type with any inference, TS will realize it is potentially infinite.也就是说,我们可以绕过它来创建一种任意深度的类型进行检查……但是如果我们尝试将这种类型与任何推理结合起来,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]
]);

I don't think variadic tuples help you here.我不认为可变元组在这里帮助你。 They will be an awesome addition to the language, but don't solve the problem that you are trying to model here.它们将是该语言的一个很棒的补充,但不要解决您在此处尝试 model 的问题。

A solution that would let you keep the best of both worlds would be to accept HTML elements as the third item in the tuple, and simply call createElements within that array.让您保持两全其美的解决方案是接受 HTML 元素作为元组中的第三项,并在该数组中简单地调用createElements

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM