Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2317,6 +2317,44 @@ const schema = Joi.array().items({ a: Joi.string() });

Possible validation errors: [`object.base`](#objectbase)

When used with TypeScript, `object` is generic; if you provide it an interface name, then Joi will check that the proper fields are defined.

```ts
interface Person {
firstName: string;
lastName: string;
}

const schema = Joi.object<Person>({
firstName: Joi.string(),
// Typo! TypeScript will report an error.
lastNam: Joi.string()
})
```

You can opt in to strict schema checking by adding `, true` to `object`'s' parameters. This instructs Joi + TypeScript to check field types as well as field names. Both Joi schemas and TypeScript types are flexible enough that there may not be a one-to-one mapping; falling back to type assertions may be necessary to satisfy some strict schema checks.

```ts
interface Person {
firstName: string;
lastName: string;
code: 0 | 1;
details: string | string[];
}

const schema = Joi.object<Person>({
firstName: Joi.number().required(),
// Should be Joi.string(). TypeScript will report an error.
lastName: Joi.number()
// Should be Joi.number().allow(0, 1).required(), but no error:
// not all errors are caught:.
code: Joi.number()
// .single() is fine but not recognized by Joi; use a
// type assertion.
details: Joi.array().items(Joi.string()).single() as any
})
```

#### `object.and(...peers, [options])`

Defines an all-or-nothing relationship between keys where if one of the peers is present, all of
Expand Down Expand Up @@ -3510,6 +3548,34 @@ Performs validation against the current schema without the extra overhead of mer

**Use this method to perform validation against nested schemas instead of `validate()`**

### TypeScript support for extensions

If you're using extensions with TypeScript and with `object`'s strict schema mapping, you can extend Joi's types to recognize added extensions. For example, if you extend Joi with a custom `myInstant` schema to add support for your codebase's `Instant` ponyfill:

```ts
// Declare an interface for your new extension that best
// represents the type.
interface MyInstantSchema extends Joi.AnySchema<Instant> {
// Add a brand so Joi + TypeScript can distinguish
__instant: boolean;
}

// Extend the Joi interface with the new methods
interface MyValidation extends Joi.Root {
instant(): MyInstantSchema;
}

// Create the new methods
const custom: MyValidation = Joi.extend(/* see above */);

// Extend schema type maps
declare module 'joi' {
interface CustomSchemaMap {
// Can use whatever for 'myInstant', as long as it's unique
myInstant: { type: Instant; schema: MyInstantSchema };
}
}
```

## Errors

Expand Down
60 changes: 39 additions & 21 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,27 +848,45 @@ declare namespace Joi {
: true
: false;

type ObjectPropertiesSchema<T = any> = true extends IsNonPrimitiveSubsetUnion<
Exclude<T, undefined | null>
>
? Joi.AlternativesSchema
: T extends NullableType<string>
? Joi.StringSchema
: T extends NullableType<number>
? Joi.NumberSchema
: T extends NullableType<bigint>
? Joi.NumberSchema
: T extends NullableType<boolean>
? Joi.BooleanSchema
: T extends NullableType<Date>
? Joi.DateSchema
: T extends NullableType<Buffer>
? Joi.BinarySchema
: T extends NullableType<Array<any>>
? Joi.ArraySchema
: T extends NullableType<object>
? StrictSchemaMap<T> | ObjectSchema<T>
: never;
// User-extensible registry for mapping value types to schema types.
// Augment via `declare module 'joi' { interface CustomSchemaMap { ... } }`,
// where each entry has the shape `{ type: SomeValue; schema: SomeSchema }`.
// Entries here are consulted before the built-in mapping, so they can also
// override the default schema chosen for a built-in type.
interface CustomSchemaMap {}

type ResolveCustomSchema<T> = {
[K in keyof CustomSchemaMap]: CustomSchemaMap[K] extends {
type: infer Type;
schema: infer S;
}
? [Exclude<T, undefined | null>] extends [Type]
? S
: never
: never;
}[keyof CustomSchemaMap];

type ObjectPropertiesSchema<T = any> = [ResolveCustomSchema<T>] extends [never]
? true extends IsNonPrimitiveSubsetUnion<Exclude<T, undefined | null>>
? Joi.AlternativesSchema
: T extends NullableType<string>
? Joi.StringSchema
: T extends NullableType<number>
? Joi.NumberSchema
: T extends NullableType<bigint>
? Joi.NumberSchema
: T extends NullableType<boolean>
? Joi.BooleanSchema
: T extends NullableType<Date>
? Joi.DateSchema
: T extends NullableType<Buffer>
? Joi.BinarySchema
: T extends NullableType<Array<any>>
? Joi.ArraySchema
: T extends NullableType<object>
? StrictSchemaMap<T> | ObjectSchema<T>
: never
: ResolveCustomSchema<T>;

type PartialSchemaMap<TSchema = any> = {
[key in keyof TSchema]?: SchemaLike | SchemaLike[];
Expand Down
49 changes: 49 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1493,3 +1493,52 @@ expect.error(Joi.string('x'));
expect.type<Record<string, unknown>>(output);
}
}

// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
// Test user-extensible schema registry (CustomSchemaMap)

declare class Instant {
private readonly __instant: true;
static now(): Instant;
}

interface MyInstantSchema extends Joi.AnySchema<Instant> {
instant(): this;
}

declare module '..' {
interface CustomSchemaMap {
Instant: { type: Instant; schema: MyInstantSchema };
}
}

const myInstant = (() => null) as unknown as () => MyInstantSchema;

interface TemporalEvent {
name: string;
at: Instant;
}

const temporalEventSchema = Joi.object<TemporalEvent, true>({
name: Joi.string().required(),
at: myInstant(),
});
expect.type<Joi.ObjectSchema<TemporalEvent>>(temporalEventSchema);

// Without the registry entry, a non-built-in type would resolve to `never` and
// reject any schema; the augmentation above is what makes the call above compile.
// Conversely, passing a wrong schema type for the registered value must error.
expect.error(
Joi.object<TemporalEvent, true>({
name: Joi.string().required(),
at: Joi.string(),
})
);

// Optional + nullable variants should still match the registered schema.
interface MaybeTemporalEvent {
at?: Instant | null;
}
Joi.object<MaybeTemporalEvent, true>({
at: myInstant(),
});