简体   繁体   中英

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 . 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).

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.

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.

(Partial answer)

TypeScript doesn't like recursive types. 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.

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. 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. See 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.

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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