Skip to content

Commit

Permalink
Support intersection types
Browse files Browse the repository at this point in the history
  • Loading branch information
iliocatallo committed Feb 2, 2024
1 parent 3ef2db7 commit 41347e6
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 1 deletion.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
- [`object`](#object)
- [`arrayOf(T)`](#arrayoft)
- [`oneOf(T1, T2, ...Ts)`](#oneoft1-t2-ts)
- [`allOf(T1, T2, ...Ts)`](#alloft1-t2-ts)
- [Utility types](#utility-types)
- [`Infer`](#infer)
- [License](#license)
Expand Down Expand Up @@ -268,6 +269,31 @@ explain(abc, 'd');
*/
```

#### `allOf(T1, T2, ...Ts)`

A run-time representation of the intersection type `T1 & T2 & ...Ts`.

```typescript
import { allOf, object, number, isValid, explain } from '@gucciogucci/contented';

const abObject = allOf(object({ a: number }), object({ b: number }));

isValid(abObject, { a: 10, b: 20 });
// true

explain(abObject, { a: 10 });
/* {
value: { a: 10 },
isNot: { allOf: [ { object: { a: 'number' } }, { object: { b: 'number' } } ] },
since: [{
value: { a: 10 },
isNot: { object: { b: 'number' } },
since: [ { missingKey: 'b' } ]
}]
}
*/
```

### Utility types

#### `Infer`
Expand Down
11 changes: 10 additions & 1 deletion src/Type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type Infer<T> = T extends Type<infer R> ? R : never
// ======================================================================
// Schema
// ======================================================================
export type Schema = PrimitiveSchema | LiteralSchema | ObjectSchema | OneOfSchema | ArrayOfSchema
export type Schema = PrimitiveSchema | LiteralSchema | ObjectSchema | OneOfSchema | AllOfSchema | ArrayOfSchema

// ----------------------------------------------------------------------
// Primitive
Expand Down Expand Up @@ -50,6 +50,15 @@ export function isOneOfSchema(schema: Schema): schema is OneOfSchema {
return typeof schema === 'object' && 'oneOf' in schema
}

// ----------------------------------------------------------------------
// AllOf
// ----------------------------------------------------------------------
export type AllOfSchema = { allOf: Schema[] }

export function isAllOfSchema(schema: Schema): schema is AllOfSchema {
return typeof schema === 'object' && 'allOf' in schema
}

// ----------------------------------------------------------------------
// Array
// ----------------------------------------------------------------------
Expand Down
69 changes: 69 additions & 0 deletions src/allOf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { test } from 'uvu'
import assert from 'uvu/assert'
import { object } from './object'
import { number } from './number'
import { isValid } from './isValid'
import { allOf } from './allOf'
import { explain } from './explain'

test(`allOf allows specifying intersections`, function () {
const T = allOf(object({ a: number }), object({ b: number }))

const res1 = isValid(T, { a: 10, b: 20 })
const res2 = isValid(T, { a: 10, b: 20, c: 30 })

assert.is(res1, true)
assert.is(res2, true)
})

test('allOf rejects input values that are not coercible to all given types', function () {
const T = allOf(object({ a: number }), object({ b: number }))

const res1 = isValid(T, { a: 10 })
const res2 = isValid(T, { b: 20 })
const res3 = isValid(T, { c: 30 })
const res4 = isValid(T, 'hello')

assert.is(res1, false)
assert.is(res2, false)
assert.is(res3, false)
assert.is(res4, false)
})

test(`there is an explanation if the input value is not coercibile to any given type`, function () {
const T = allOf(object({ a: number }), object({ b: number }))

const exp1 = explain(T, { a: 10 })
const exp2 = explain(T, { c: 30 })

assert.equal(exp1, {
value: { a: 10 },
isNot: { allOf: [{ object: { a: 'number' } }, { object: { b: 'number' } }] },
since: [
{
value: { a: 10 },
isNot: { object: { b: 'number' } },
since: [{ missingKey: 'b' }],
},
],
})

assert.equal(exp2, {
value: { c: 30 },
isNot: { allOf: [{ object: { a: 'number' } }, { object: { b: 'number' } }] },
since: [
{
value: { c: 30 },
isNot: { object: { a: 'number' } },
since: [{ missingKey: 'a' }],
},
{
value: { c: 30 },
isNot: { object: { b: 'number' } },
since: [{ missingKey: 'b' }],
},
],
})
})

test.run()
19 changes: 19 additions & 0 deletions src/allOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Infer, Type } from './Type'

export function allOf<T1 extends Type<unknown>, T2 extends Type<unknown>, Ts extends Type<unknown>[]>(
first: T1,
second: T2,
...rest: Ts
): Type<AllOf<[T1, T2, ...Ts]>> {
const allOf = [first, second, ...rest].map((type) => type.schema)
return { schema: { allOf } }
}

type AllOf<Types, P = unknown, O = unknown> = Types extends [infer Head, ...infer Rest]
? Infer<Head> extends object
? AllOf<Rest, P, Infer<Head> & O>
: AllOf<Rest, Infer<Head> & P, O>
: P & PrettifyObj<O>
type PrettifyObj<T> = {
[K in keyof T]: T[K]
} & {}
21 changes: 21 additions & 0 deletions src/explain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { typeOf } from './typeOf'
import {
AllOfSchema,
ArrayOfSchema,
isAllOfSchema,
isArrayOfSchema,
isLiteralSchema,
isObjectSchema,
Expand Down Expand Up @@ -32,6 +34,9 @@ function explainSchema(schema: Schema, value: any): Explanation | undefined {
if (isOneOfSchema(schema)) {
return explainOneOf(schema, value)
}
if (isAllOfSchema(schema)) {
return explainAllOf(schema, value)
}
if (isArrayOfSchema(schema)) {
return explainArrayOf(schema, value)
}
Expand Down Expand Up @@ -98,6 +103,22 @@ function explainOneOf(schema: OneOfSchema, value: any): Explanation | undefined
}
}

function explainAllOf(schema: AllOfSchema, value: any): Explanation | undefined {
const schemas = schema.allOf
const since: NestedExplanation[] = []
for (const instSchema of schemas) {
const exp = explainSchema(instSchema, value)
if (exp) {
since.push(exp)
}
}
return {
value,
isNot: schema,
since,
}
}

function explainArrayOf(schema: ArrayOfSchema, value: any): Explanation | undefined {
if (!Array.isArray(value)) {
return {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { allOf } from './allOf'
export { arrayOf } from './arrayOf'
export { boolean } from './boolean'
export { explain } from './explain'
Expand Down
16 changes: 16 additions & 0 deletions src/isValid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
OneOfSchema,
isArrayOfSchema,
ArrayOfSchema,
isAllOfSchema,
AllOfSchema,
} from './Type'

export function isValid<R>(type: Type<R>, value: any): value is R {
Expand All @@ -32,6 +34,9 @@ function isValidSchema(schema: Schema, value: any): boolean {
if (isOneOfSchema(schema)) {
return isValidOneOf(schema, value)
}
if (isAllOfSchema(schema)) {
return isValidAllOf(schema, value)
}
if (isArrayOfSchema(schema)) {
return isValidArrayOf(schema, value)
}
Expand Down Expand Up @@ -94,3 +99,14 @@ function isValidOneOf(schema: OneOfSchema, value: any): boolean {
}
return false
}

function isValidAllOf(schema: AllOfSchema, value: any): boolean {
const schemas = schema.allOf
for (const interSchema of schemas) {
const valid = isValidSchema(interSchema, value)
if (!valid) {
return false
}
}
return true
}

0 comments on commit 41347e6

Please sign in to comment.