Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to infer generic types recursively in TypeScript?

Tags:

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.

like image 258
rafaelgomesxyz Avatar asked Jun 24 '20 21:06

rafaelgomesxyz


People also ask

How to define generic type in TypeScript?

TypeScript Generics is a tool which provides a way to create reusable components. It creates a component that can work with a variety of data types rather than a single data type. It allows users to consume these components and use their own types.

Does TypeScript have generics?

TypeScript fully supports generics as a way to introduce type-safety into components that accept arguments and return values whose type will be indeterminate until they are consumed later in your code.


Video Answer


1 Answers

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

like image 141
Gerrit0 Avatar answered Oct 29 '22 07:10

Gerrit0