sinclairzx81 / typebox-workbench

Type Transform Tool for Runtime Type Systems
Other
49 stars 6 forks source link

Better support for record types #15

Closed hpeebles closed 1 week ago

hpeebles commented 2 weeks ago

Please see the example below.

Input:

export type MyFancyRecord = {
  [key: string]: string
}

export type MyOtherFancyRecord = {
  [key: number]: string
}

Output:

import { Type, Static } from '@sinclair/typebox'

export type MyFancyRecord = Static<typeof MyFancyRecord>
export const MyFancyRecord = Type.Object(
  {},
  {
    additionalProperties: Type.String()
  }
)

export type MyOtherFancyRecord = Static<typeof MyOtherFancyRecord>
export const MyOtherFancyRecord = Type.Object(
  {},
  {
    additionalProperties: Type.String()
  }
)

Desired output:

import { Type, Static } from '@sinclair/typebox'

export type MyFancyRecord = Static<typeof MyFancyRecord>
export const MyFancyRecord = Type.Record(
  Type.String(),
  Type.String()
)

export type MyOtherFancyRecord = Static<typeof MyOtherFancyRecord>
export const MyOtherFancyRecord = Type.Record(
  Type.Number(),
  Type.String()
)
sinclairzx81 commented 2 weeks ago

@hpeebles Hi, you can generate the correct types using the following.

export type MyFancyRecord = Record<string, string>

export type MyOtherFancyRecord = Record<number, string>

The output you see for { [key: string]: string } is a current design limitation in TypeBox.

Consider the following...

type T = {
   [key: string]: number, // additionalProperties
   x: number,
   y: number,
   z: number
}

which maps into the following

type T = Static<typeof T>
const T = Type.Object(
  {
    x: Type.Number(),
    y: Type.Number(),
    z: Type.Number()
  },
  {
    additionalProperties: Type.Number()
  }
)

... but where additionalProperties does not currently contribute to type inference (which is at the core of this issue). For now the recommendation is to use Record instead of the literal type expression.


Additionally, If you need a structure similar to the T type above, you can use.

type T = {
   x: number,
   y: number,
   z: number
} & Record<string, number>

which maps to

type T = Static<typeof T>
const T = Type.Intersect([
  Type.Object({
    x: Type.Number(),
    y: Type.Number(),
    z: Type.Number()
  }),
  Type.Record(Type.String(), Type.Number())
])

The Workbench will be updated with a solution to this once TypeBox implements additional inference logic for additionalProperties.

Hope this helps S

hpeebles commented 1 week ago

Ahh amazing! Thanks for the help!

I'm actually generating the Typescript types from Rust using ts-rs, so I guess now I need to try and get that to output Record's rather than using the current [key: string] format.