sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.56k stars 148 forks source link

Transform type keys in Record #919

Closed s-egea closed 1 week ago

s-egea commented 2 weeks ago

Behavior

Hi, I've encountered the following behavior using TypeBox

import * as T from "@sinclair/typebox";

const K = T.Transform(T.String())
  .Decode((v) => v)
  .Encode((v) => v);

const T = T.Record(K, T.String());

type Res = T.Static<typeof T>; // gives never

It seems that using Transform types in Record keys is not yet supported.

Patch

For now I've applied the following patch to make my code work (my decoding function is just the identity function, so all I really need is the Static type to work).

export type TRecordOrObject_<K extends TSchema, T extends TSchema> =
  K extends TTemplateLiteral ? TFromTemplateLiteralKey<K, T> :
  K extends TEnum<infer S> ? TFromEnumKey<S, T> : // (Special: Ensure resolve Enum before Union)
  K extends TUnion<infer S> ? TFromUnionKey<S, T> :
  K extends TLiteral<infer S> ? TFromLiteralKey<S, T> :
  K extends TInteger ? TFromIntegerKey<K, T> :
  K extends TNumber ? TFromNumberKey<K, T> :
  K extends TRegExp ? TFromRegExpKey<K, T> :
  K extends TString ? TFromStringKey<K, T> :
  TNever
// ------------------------------------------------------------------
// TRecordOrObject
// ------------------------------------------------------------------
/** `[Json]` Creates a Record type */
export type TRecordOrObject<K extends TSchema, T extends TSchema> = K extends TTransform<infer R, infer _> ? TRecordOrObject_<R, T> : TRecordOrObject_<K, T> // patch
export function Record<K extends TSchema, T extends TSchema>(K: K, T: T, options: ObjectOptions = {}): TRecordOrObject<K, T> {
  // prettier-ignore
  return (
    IsUnion(K) ? FromUnionKey(K.anyOf, T, options) :
    IsTemplateLiteral(K) ? FromTemplateLiteralKey(K, T, options) :
    IsLiteral(K) ? FromLiteralKey(K.const, T, options) :
    IsInteger(K) ? FromIntegerKey(K, T, options) :
    IsNumber(K) ? FromNumberKey(K, T, options) :
    IsRegExp(K) ? FromRegExpKey(K, T, options) :
    IsString(K) ? FromStringKey(K, T, options) :
    Never(options)
  ) as never
}

I wonder if it would make sense to add support for using Transform types in Record keys (not just at the type level) ?

Thanks for your answer !

sinclairzx81 commented 1 week ago

@s-egea Hi! Sorry for the delay.

I wonder if it would make sense to add support for using Transform types in Record keys (not just at the type level) ?

Hmmm, I'm not too sure about this. There isn't really an mechanism that would enable Record keys to be transformed (either for Encode/Decode), and the best next thing I can think of would be to discard the Transform when applied to keys (which may not be expected).

Currently, the Record type only accepts a few known key types (string, number, union literal, template literal, generally any type that can naturally encode the type as part of the patternProperties sub schematic). Given that Transforms augment the schematic in ways that cannot encode to patterns, it generally falls outside a supported type (even if the type happens to be String). I think on this basis, I think it's correct to deem transforms as non-viable record keys which should reflect in the inference as TNever

const K = T.Transform(T.String())
  .Decode((v) => v)
  .Encode((v) => v);

const S = T.Record(K, T.String()); // S is T.TNever

Will close out this issue for now as I don't think there is much that can be done to resolve in a satisfactory way that makes sense for Transforms (as they currently are). But I'll drop a suggestion tag on this issue and revisit in future. There are planned updates to Transforms (and validation) in later releases, and there may be potential to explore this under a updated validation infrastructure.

Cheers! S

s-egea commented 1 week ago

No worries about the delay!

I’m just dropping some ideas on how I thought it could be implemented. I apologize in advance if my understanding is incorrect or incomplete.

As I find it difficult to explain my ideas using text only, I will share two drawings instead:

TB_types

transform_keys

I'm aware that this use case is not very common, so I’m perfectly fine continuing with my (type) patch for now.

There are planned updates to Transforms (and validation) in later releases, and there may be potential to explore this under an updated validation infrastructure.

Note that my current project makes extensive use of Transform types, so I will be happy to share some feedback and pain points I encountered.

Thanks again!