sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.77k stars 152 forks source link

TUnion of literals from const array #751

Closed ehaynes99 closed 7 months ago

ehaynes99 commented 7 months ago

I've been trying -- unsuccessfully -- to come up with a function to create a TUnion using a const array (or a TS 5.x "const parameter" like <const T extends ReadonlyArray<string | number | boolean>>). E.g.

const values = ['first', 'second', 'third'] as const

// equivalent to:
// const AsTUnion: TUnion<[TLiteral<"first">, TLiteral<"second">, TLiteral<"third">]>
const AsTUnion = Type.Union([Type.Literal('first'), Type.Literal('second'), Type.Literal('third')])

I thought it would be straightforward:

export type UnionValues<T extends ReadonlyArray<string | number | boolean>> = {
  [K in keyof T]: TLiteral<T[K]>
}

// inferred as:
// type Tmp = readonly [TLiteral<"first">, TLiteral<"second">, TLiteral<"third">]
type Tmp = UnionValues<typeof values>

However, this doesn't satisfy the generics for TUnion

const LiteralUnion = <const T extends ReadonlyArray<string | number | boolean>>(
  values: T,
// typescript: Type 'UnionValues<T>' does not satisfy the constraint 'TSchema[]'. [2344]
): TUnion<UnionValues<T>> => // ...

I tried approaching from the other direction:

// inferred as:
// type Tmp2 = TLiteral<"first" | "second" | "third">
type Tmp2 = TLiteral<(typeof values)[number]>

But would have to use an unsafe type to actually create it, e.g.:

const LiteralUnion = <const T extends ReadonlyArray<string | number | boolean>>(values: T): TLiteral<T[number]> => {
  return Type.Unsafe<T[number]>(Type.Union(values.map((str) => Type.Literal(str)))) as any
}

// const Example: TLiteral<"first" | "second" | "third">
const Example = LiteralUnion(values)
// type Example = "first" | "second" | "third"
type Example = Static<typeof Example>

This works, but feels rather ugly, as TypeBox doesn't really have a native equivalent. Any ideas?

sinclairzx81 commented 7 months ago

@ehaynes99 Hi, try the following

TypeScript Link Here

import { Type, TUnion, TLiteral } from '@sinclair/typebox'

export type TLiteralUnion<T extends string[], Acc extends TLiteral[] = []> = 
  T extends [infer L extends string, ...infer R extends string[]]
    ? TLiteralUnion<R, [...Acc, TLiteral<L>]>
    : TUnion<Acc>

function LiteralUnion<T extends string[]>(values: readonly [...T]): TLiteralUnion<T> {
  return Type.Union(values.map(value => Type.Literal(value))) as never
}

// ...

const V = ['first', 'second', 'third'] as const

const T = LiteralUnion(V)

Hope this helps S

ehaynes99 commented 7 months ago

It does indeed. Thanks, you're the best!