Closed EvHaus closed 1 month ago
@EvHaus Hi,
Is this a bug, or a limitation of typebox?
It's neither. The issue is that DATA.map returns an array but where TypeBox expects a fixed size tuple for Union (and other types). Your first example works because you have explicitly passed an array with a known size (which TypeScript can observe as a tuple), the second example does not as the .map() function returns an array where interior elements are taken as a union. Consider.
const A = [1, 2, 3, 4] as const // type A = readonly [1, 2, 3, 4]
const B = A.map(value => value) // type B = (1 | 2 | 3 | 4)[] - (no longer a tuple)
Here is the equivalent when mapping for TLiteral types.
const DATA = [{value: 'Bob'}, {value: 'Stella'}] as const;
const VARIANTS = DATA.map(({ value }) => Type.Literal(value)) // hover VARIANTS
//
// TLiteral<"Bob" | "Stella">[] -- Incorrect
//
// [TLiteral<'Bob'>, TLiteral<'Stella'>] -- Correct
TypeBox does not support the TLiteral<"Bob" | "Stella">[]
form as it's very expensive to derive the constituents of "Bob" | "Stella"
within the type system. The solution instead is to write a little bit of type level programming that maps each element of DATA 1 by 1 which proves the compiler both the mapping and size of the array (making it a tuple). The following achieves this.
import { Type, TLiteral } from '@sinclair/typebox'
// --------------------------------------------------------------------------------
// DataToLiteralTuple
// --------------------------------------------------------------------------------
type Element = { value: string }
// This is a .map() operation in the type system
type TDataToLiteralTuple<Data extends Element[], Result extends TLiteral[] = []> = (
Data extends [infer L extends Element, ...infer R extends Element[]]
? TDataToLiteralTuple<R, [...Result, TLiteral<L['value']>]>
: Result
)
function DataToLiteralTuple<Data extends Element[]>(elements: readonly [...Data]): TDataToLiteralTuple<Data> {
return elements.map(element => Type.Literal(element.value)) as TDataToLiteralTuple<Data>
}
// --------------------------------------------------------------------------------
// Example
// --------------------------------------------------------------------------------
const DATA = [{value: 'Bob'}, {value: 'Stella'}] as const;
const thing = Type.Union(DataToLiteralTuple(DATA))
Above, you would use DataToLiteralTuple
instead of DATA.map
as this function returns the correct computed tuple. The above pattern is fairly common when mapping external data structures to TypeBox types, but does require a little bit of type level programming to implement correctly.
Hope this helps S
Came to report a bug. Got an amazing TypeScript lesson instead. Thanks so much for the super fast response and detailed answer @sinclairzx81!
I'm trying to figure out why I don't get literal types when using a
.map()
insideT.Union()
. Consider this code:However, this doesn't work:
Is this a bug, or a limitation of typebox?