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
The nullable option is not reflected in TypeScript types #38
Comments
@sunaurus Hi Yeah, TypeBox doesn't currently try and infer types from the additional options passed on the types (i.e. Do you have some documentation / specifications available for |
You're right, I assumed that it was JSON schema but it's actually just an OpenAPI thing. My mistake! |
@sinclairzx81 while this is indeed an extension supported by OpenAPI, it would be very useful to me. I don't think it should be in the base library but maybe you can comment on how I could add this functionality locally. I was initially going for something like this: class NullableTypeBuilder extends TypeBuilder {
NullableString<TCustomFormatOption extends string>(
options?: StringOptions<StringFormatOption | TCustomFormatOption>,
): TOptional<TString> {
return {
...options,
kind: exports.StringKind,
modifier: exports.OptionalModifier,
nullable: true,
type: 'string',
};
}
}
const MyType = new NullableTypeBuilder();
export const testSchema = MyType.Object({
display_name: MyType.NullableString(),
});
export type TestType = Static<typeof testSchema>; however type TestType = {} & {} & {
display_name?: string;
} & {} instead of type TestType = {} & {} & {
display_name: string | null;
} & {} I'm not sure how to do the latter without forking Typebox, any ideas? |
@Nepoxx Hi, You can achieve this with the following examples, but you will need to run the typescript compiler in import { Type, Static } from '@sinclair/typebox'
const T = Type.Object({
display_name: Type.Union([Type.String({ nullable: true }), Type.Null()])
})
type T = Static<typeof T>
//
// type T = {} & {} & {} & {
// display_name: string | null;
// } Or, if you want to construct a reusable type for nullable strings, you can do something like. import { Type, Static, StringFormatOption, StringOptions } from '@sinclair/typebox'
// The following is the full signature for a kind of `nullable` string. The generic `CustomFormat`
// allows passing additional custom formats for string validation above an beyond the built-in JSON schema
// string format validation types.
const NullableString = <CustomFormat extends string>(options?: StringOptions<StringFormatOption | CustomFormat>) => {
return Type.Union([Type.String({ nullable: true, ...options }), Type.Null()])
}
const T = Type.Object({
display_name: NullableString(),
display_email: NullableString({ format: 'email' })
})
type T = Static<typeof T>
// type T = {} & {} & {} & {
// display_name: string | null;
// display_email: string | null;
// } Also, it's possible to extend (sub class) the |
Thanks for the quick reply :) The issues is that i.e. {
type: 'object'
properties: {
foo: {
anyOf: [
{type: 'string'},
{type: 'null'}
]
}
}
}
// vs
{
type: 'object'
properties: {
foo: { type: ['string', 'null'] }
}
} the main difference comes from how the spec says one should be validated and how types are coerced. My ideal is having {
type: 'object'
properties: {
foo: { type: 'string', nullable: true }
}
} and having the corresponding TS type: type Foo = {
foo: string | null;
} |
@Nepoxx Thanks for the response. I am open to amending the output JSON schema for better (or more terse) alignment with the TS type checker, however it would need to be in tune with the JSON schema specification. Unfortunately, the I'm not sure why the Swagger team decided to diverge from the JSON schema specification, but its a little frustrating they did. I'm guessing the Open API I could recommend you perhaps forking TypeBox and having a go at adding this in. My advice might be to create a new const T = Type.Object({
foo: Type.Nullable(Type.String())
})
const T = {
"type": "object",
"properties": {
"foo": { "type" : "string", "nullable": true }
},
"required": ["foo"]
} Where export type StaticNullable<T extends TSchema> = Static<T> | null Hope this helps |
Is there still no way to add nullables to JSON schema today? I'm refactoring a codebase from hand-written TS types, and now I'm forced to use
No workaround besides Open API? (which breaks JSON schema...) |
According to this answer, it seems we want Since the Could we add support for the class TypeBuilder {
// ...
Store(schema) {
if (schema.nullable === true && typeof schema.type === "string") {
schema = { ...schema, type: [schema.type, "null"] };
}
// ...
return $schema;
}
// ...
} |
@mindplay-dk Hi. Unfortunately OpenAPI's https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0 Note that Nullable types are possible with TypeBox using JSON schema representations. TypeBox constructs these from unions the same way TypeScript does. So for example, the follow three definitions are equivalent. // typebox
const T = Type.Union([Type.String(), Type.Null()])
// typescript
type T = string | null
// schema
const T = {
anyOf: [
{ type: 'string' },
{ type: 'null' }
]
} Currently, TypeBox provides a fallback mechanism for OpenAPI prior to 3.1 where you can construct a custom schema representation for nullable. Example below and link here import { Type, Static, TSchema, TUnion, TNull } from '@sinclair/typebox'
/** Augments the given schema with the 'nullable' and returns a (T | null) */
function Nullable<T extends TSchema>(schema: T): TUnion<[TNull, T]> {
return { ...schema, nullable: true } as any // facade
}
const T = Type.Object({
email: Nullable(Type.String())
})
type T = Static<typeof T> As for the I expect things will be a bit easier from Open API 3.1 and above. Hope this helps |
Well, I saw that elsewhere, but as someone pointed out, these are not exactly equivalent to For now, I went with this helper function: function Nullable<T extends TSchema>(schema: T): TUnion<[T, TNull]>
function Nullable<T extends TSchema>(schema: any): any {
if (typeof schema.type !== "string") {
throw new Error(`This type cannot be made nullable`);
}
schema.type = [schema.type, "null"];
return schema;
} It's not what I proposed above, but it seems to work. We might be able to merge these ideas? So instead of the It would be nice to have something that built-in that "just works". |
(for the record, I'm not using Open API - this needs to be standard JSON schema.) |
Is there a functional difference between the following schema representations? const T = { type: ['string', 'null'] } and const T = { anyOf: [{ type: 'string'}, { type: 'null' }] } For composition sake, TypeBox opts for the latter (as it permits the union of two objects), resulting in the following. const T = {
anyOf: [
{ type: 'object', properties: { a: { type: 'string' } } },
{ type: 'object', properties: { b: { type: 'string' } } },
]
} The above is not trivially expressible using If there is a functional disparity between Open to your thoughts. |
Good question. My main concern was that quicktype.io might interpret these differently - but it doesn't appear to: Ran it for a bunch of languages, and don't see any difference. I guess the only practical reason to prefer So yeah, I agree with your conclusion, I think. 🙂 Well, shouldn't we have a
But it takes quite a bit of thinking to arrive at this conclusion. Might be better to just have it in there, so no one has to learn from documentation how to do it correctly? Must be a fairly common requirement. |
Yeah I understand, and I do actually get asked this quite a lot; usually from Open API users seeking out ways to express nullable types. My reluctance to include something as default mostly stems from ensuring that TypeBox functions always produce valid JSON schema output (which rules out the With respect to For example, TypeScript provides no type for type Nullable<T> = T | null
const Nullable = <T extends TSchema>(t: T) => Type.Union([t, Type.Null()]) I do actually intend to provide better documentation in the future around some of the more sophisticated things you can do with TypeBox with the TS analogs. I'm hoping to get something up before the end of the year. Hope this helps |
Hi! I wonder whether there's a way to make nullable (union-ish) schema checks more consistent? Value.Decode(Type.String(), 123) // ERR: Expected string -- GOOD
Value.Decode(Nullable(Type.String()), 123) // ERR: Expected union value -- IMO BAD TIA |
results in:
Is it even possible to fix this?
I realize that the recommended workaround is to use
Type.Union()
withType.Null()
, but this does not play well with some other libraries (among other reasons due to #37) and is actually considered an anti-pattern by some people: ajv-validator/ajv#1107 (comment)The text was updated successfully, but these errors were encountered: