hapijs / hapijs/joi

TypeScript distributive conditional causes strings to pass as optional properties in object schemas

Open
#2,644 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
21.2k
Forks
1.5k
Avg merge
4h 57m
Merged PRs (30d)
14

Description

#### Support plan

* *is this issue currently blocking your project?* (yes/no): no
* *is this issue affecting a production system?* (yes/no): no

#### Context

* *node version*: v14.15.3
* *module version with issue*: v17.4.1
* *last module version without issue*: N/A
* *environment* (e.g. node, browser, native): node
* *used with* (e.g. hapi application, another framework, standalone, ...): koa
* *any other relevant information*:

#### What are you trying to achieve or the steps to reproduce?

While using Joi with TypeScript, when creating a strict object schema the programmer is able to set any optional or nullable property to a Joi.string() schema without the compiler showing an error.

#### What was the result you got?

```ts
interface User {
acceptedTerms: boolean;
age?: number;
isPro?: boolean;
coinCount: number | null;
}

const userSchema = Joi.object({
acceptedTerms: Joi.string(), // error, StringSchema is not assignable to BooleanSchema
age: Joi.string(), // no error, resolves to StringSchema
isPro: string(), // no error, resolves to StringSchema
coinCount: Joi.string(), // no error, resolves to StringSchema
});
```

#### What result did you expect?

It should show an error in the three cases above (`age`, `isPro`, `coinCount`) telling the programmer that the types do not match the specified interface.

I believe the problem is caused by the `ObjectPropertiesSchema` type in the type definitions and is related to TypeScript's [Distributive Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html?#distributive-conditional-types).

```ts
type NullableType = undefined | null | T

type ObjectPropertiesSchema =
T extends NullableType
? Joi.StringSchema
: T extends NullableType
? Joi.NumberSchema
: T extends NullableType
? Joi.NumberSchema
: T extends NullableType
? Joi.BooleanSchema
: T extends NullableType>
? Joi.ArraySchema
: T extends NullableType
? ObjectSchema>
: never
```

The resolver runs the conditional type definition for each type within T (if it is a union type). For example: if T is `number | undefined` then the resolver will run `ObjectPropertiesSchema` and `ObjectPropertiesSchema` which results in `Joi.StringSchema | Joi.NumberSchema` but I believe it should only result in `Joi.NumberSchema`.

A solution I found was to surround each side of the `extends` keyword with square brackets as specified in the [documentation](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html?#distributive-conditional-types):

```ts
type NullableType = undefined | null | T

type ObjectPropertiesSchema =
[T] extends [NullableType]
? Joi.StringSchema
: [T] extends [NullableType]
? Joi.NumberSchema
: [T] extends [NullableType]
? Joi.NumberSchema
: [T] extends [NullableType]
? Joi.BooleanSchema
: [T] extends [NullableType>]
? Joi.ArraySchema
: [T] extends [NullableType]
? ObjectSchema>
: never
```

Another (more simple) solution would be to remove the `NullableType` entirely:

```ts
type ObjectPropertiesSchema =
T extends string
? Joi.StringSchema
: T extends number
? Joi.NumberSchema
: T extends bigint
? Joi.NumberSchema
: T extends boolean
? Joi.BooleanSchema
: T extends Array
? Joi.ArraySchema
: T extends object
? ObjectSchema>
: never
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.