sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.79k stars 153 forks source link

Static type precision loss for Type.Enum in Type.TemplateLiteral #444

Closed s-egea closed 1 year ago

s-egea commented 1 year ago

Static type resulting from the nesting of a Type.Enum in a Type.TemplateLiteral transforms original const literal type into a more general string type. Here is a small reproduction:

import { Type, Static } from "@sinclair/typebox";

const U = Type.Union([Type.Literal("A"), Type.Literal("B")]);
const E = Type.Enum(Object.freeze({ a: "A", b: "B" }));

type U = Static<typeof U>; // "A" | "B"
type E = Static<typeof E>; // "A" | "B"

const TU = Type.TemplateLiteral([U]);
const TE = Type.TemplateLiteral([E]);

type TU = Static<typeof TU>; // "A" | "B"
type TE = Static<typeof TE>; // string

As far as I can understand this behavior comes from this part of the code (src/typebox.ts)

export type TTemplateLiteralConst<T, Acc extends string> = 
  T extends TUnion<infer U> ? { [K in keyof U]: TTemplateLiteralUnion<Assert<[U[K]], TTemplateLiteralKind[]>, Acc> }[number] :
  T extends TTemplateLiteral ? `${Static<T>}` :

where type U get inferred as a TLiteral<string | number>[] when T is a TEnum.

The following change seems to produce the desired behavior (src/typebox.ts):

export interface TEnum<T extends Record<string, string | number> = Record<string, string | number>> extends TSchema {
  [Kind]: 'Union'
  static: T[keyof T]
  // anyOf: TLiteral<string | number>[]
  anyOf: TLiteral<T[keyof T]>[]
}
sinclairzx81 commented 1 year ago

@s-egea Hi! this is good spotting!

I really liked your solution and have applied it on update on 0.28.12. Tested both enum and your object literal definition, and it looks like it works great! Nice work :)

Will close off this issue for now, but if you spot any problems, feel free to let me know on this thread. All the best! S

s-egea commented 1 year ago

Thanks for the quick reply and update ! 👍