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.
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.
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.
(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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With