Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to have an Object+Dict? #60

Closed
lazharichir opened this issue Mar 21, 2021 · 6 comments
Closed

How to have an Object+Dict? #60

lazharichir opened this issue Mar 21, 2021 · 6 comments

Comments

@lazharichir
Copy link

Hello, basically the desire here is to get the below type:

type Metadata = {
	provider: "gcs" | "s3"
	bucket: string
	name: string
	[key: string]: any
}

I've tried to do:

const Metadata = Type.Intersect(
	[
		Type.Dict(Type.Any()),
		Type.Object({
			provider: Type.Union([Type.Literal(`gcs`), Type.Literal(`s3`)]),
			bucket: Type.String({ minLength: 1 }),
			name: Type.String({ minLength: 1 }),
		})
	]
)

But it gives me an error on Type.Dict(Type.Any()),:

Type 'TDict<TAny>' is not assignable to type 'TObject<TProperties>'.
  Property 'properties' is missing in type 'TDict<TAny>' but required in type '{ kind: unique symbol; type: "object"; additionalProperties: false; properties: TProperties; required?: string[] | undefined; }'.ts(2322)
typebox.d.ts(78, 5): 'properties' is declared here.

Let me know if I have missed something!

Thank you!

@sinclairzx81
Copy link
Owner

@lazharichir Hi, good spotting. There has recently been some updates to Intersect handling in TypeBox where the Type.Intersect(...) function now constrains to TObject<TProperties> types only. This was due to a recent schema update where additionalProperties: false is set for all objects (which implicated the previous intersect handling).

Note a Type.Dict(...) when intersected with a Type.Object(...) implies additionalProperties: true which is where this issue originates. However, your implementation looks good to me, so I'm going to flag this as a bug. I may not be able to look at this for a week or so. As a work around, could something like the following work for you?

const Metadata = Type.Object({
  provider: Type.Union([Type.Literal(`gcs`), Type.Literal(`s3`)]),
  bucket: Type.String({ minLength: 1 }),
  name: Type.String({ minLength: 1 }),
  extra: Type.Dict(Type.Any()) // should work ok
})

Thanks for letting me know!
S

@lazharichir
Copy link
Author

No worries! In the meantime I will use an untyped Dict and will type it later once this is fixed.

Thanks for the quick reply @sinclairzx81 !

@sinclairzx81
Copy link
Owner

@lazharichir Have taken another look at this tonight and I don't think this is a trivially resolvable issue. I have explored a couple of approaches, one constraining to TObject<TProperties> | TDict<...> (which lead to a considerable amount of verbosity trying to infer from the Dict case) and another (currently sitting inside the unevaluatedProperties branch) which explores a 2019 draft proposal that would allow for a more tenable schema representation for Intersect. However it doesn't solve the Dict case where the expectation is you have some defined properties, with the additionalProperties conforming to the Dict.

Am going to move this issue from bug to enhancement and explore some other options at some point down the road.

@ajotaos
Copy link

ajotaos commented Apr 17, 2021

+1 would love this feature as well 🙂

@sinclairzx81
Copy link
Owner

Hi everyone. Will be closing off this issue shortly as I've just published a new version of TypeBox that deprecates Type.Dict(...) in favor of a new Type.Record(K, V) type which is analogous to TypeScript's utility type Record<Keys, Value> which you can find information on here.

The Type.Record(...) type works the same as the Type.Dict(...) except you can now pass a type for the Key. Valid key types are Type.String(), Type.Number() and Type.Union([...Type.Literal(...)])

String Keys

type T = Record<string, number> // typescript

const T = Type.Record(Type.String(), Type.Number()) // typebox - replaces Type.Dict()

type T = { [key: string]: number } // static

Number Keys

type T = Record<number, number> // typescript

const T = Type.Record(Type.Number(), Type.Number()) // typebox

type T = { [key: number]: number } // static

Literal Keys

type T = Record<'a' | 'b' | 'c', number> // typescript

const K = Type.Union([
  Type.Literal('a'),
  Type.Literal('b'),
  Type.Literal('c'),
])

const T = Type.Record(K, Type.Number()) // typebox

type T = {
  a: number,
  b: number,
  c: number
} // static

Unfortunately, I have not found a solution to allow Type.Object(...) to intersect how I would prefer for Type.Dict(...) or Type.Record(...) types as JSON schema can't adequately validate in the same way as TypeScript can intersect. However, I've removed the TS constraint that prevent Type.Record() from being intersected, and there are some compositions that work well so long as you keep unevaluatedProperties and additionalProperties as undefined. Mileage may vary, but you're free to try :)

Thanks for raising this issue. It's been quite the journey figuring out how to provide better support for this, so I hope the new Type.Record(...) serves as a reasonable compromise given the constraints.

Many Thanks
S

@lazharichir
Copy link
Author

I'll check the new types out, thanks for trying your best.

Stay well,

L

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants