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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for "oneOf" to TypeScript typings #168

Merged
merged 1 commit into from Aug 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 27 additions & 7 deletions index.d.ts
@@ -1,4 +1,4 @@
type AnySchema = NullSchema | BooleanSchema | NumberSchema | StringSchema | AnyEnumSchema | AnyArraySchema | AnyObjectSchema | AnyAllOptionalObjectSchema
type AnySchema = NullSchema | BooleanSchema | NumberSchema | StringSchema | AnyEnumSchema | AnyArraySchema | AnyObjectSchema | AnyAllOptionalObjectSchema | AnyOneOfSchema
type StringKeys<T> = (keyof T) & string

interface NullSchema {
Expand Down Expand Up @@ -43,6 +43,8 @@ interface AllOptionalObjectSchema<Properties extends Record<string, AnySchema>>
properties: Properties
}

interface AnyOneOfSchema { oneOf: AnySchema[] }

interface ArrayFromSchema<ItemSchema extends AnySchema> extends Array<TypeFromSchema<ItemSchema>> {}

type ObjectFromSchema<Properties extends Record<string, AnySchema>, Required extends StringKeys<Properties>> = {
Expand Down Expand Up @@ -76,16 +78,34 @@ interface Validator<Schema extends AnySchema, Output = TypeFromSchema<Schema>> {
toJSON(): Schema
}

interface Filter<Output> {
interface Filter<Schema extends AnySchema, Output = TypeFromSchema<Schema>> {
(input: Output, options?: any): Output
}

interface Factory {
<Properties extends Record<string, AnySchema>, Required extends StringKeys<Properties>> (schema: ObjectSchema<Properties, Required>, options?: any): Validator<ObjectSchema<Properties, Required>>
<Schema extends AnySchema> (schema: Schema, options?: any): Validator<Schema>

createFilter<Properties extends Record<string, AnySchema>, Required extends StringKeys<Properties>> (schema: ObjectSchema<Properties, Required>, options?: any): Filter<ObjectFromSchema<Properties, Required>>
createFilter<Schema extends AnySchema> (schema: Schema, options?: any): Filter<TypeFromSchema<Schema>>
/* One of object schema */
<Properties1 extends Record<string, AnySchema>, Required1 extends StringKeys<Properties1>, Properties2 extends Record<string, AnySchema>, Required2 extends StringKeys<Properties2>> (schema: { oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>] }, options?: any): Validator<{ oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>] }, ObjectFromSchema<Properties1, Required1> | ObjectFromSchema<Properties2, Required2>>
createFilter<Properties1 extends Record<string, AnySchema>, Required1 extends StringKeys<Properties1>, Properties2 extends Record<string, AnySchema>, Required2 extends StringKeys<Properties2>> (schema: { oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>] }, options?: any): Filter<{ oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>] }, ObjectFromSchema<Properties1, Required1> | ObjectFromSchema<Properties2, Required2>>
<Properties1 extends Record<string, AnySchema>, Required1 extends StringKeys<Properties1>, Properties2 extends Record<string, AnySchema>, Required2 extends StringKeys<Properties2>, Properties3 extends Record<string, AnySchema>, Required3 extends StringKeys<Properties3>> (schema: { oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>] }, options?: any): Validator<{ oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>] }, ObjectFromSchema<Properties1, Required1> | ObjectFromSchema<Properties2, Required2> | ObjectFromSchema<Properties3, Required3>>
createFilter<Properties1 extends Record<string, AnySchema>, Required1 extends StringKeys<Properties1>, Properties2 extends Record<string, AnySchema>, Required2 extends StringKeys<Properties2>, Properties3 extends Record<string, AnySchema>, Required3 extends StringKeys<Properties3>> (schema: { oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>] }, options?: any): Filter<{ oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>] }, ObjectFromSchema<Properties1, Required1> | ObjectFromSchema<Properties2, Required2> | ObjectFromSchema<Properties3, Required3>>
<Properties1 extends Record<string, AnySchema>, Required1 extends StringKeys<Properties1>, Properties2 extends Record<string, AnySchema>, Required2 extends StringKeys<Properties2>, Properties3 extends Record<string, AnySchema>, Required3 extends StringKeys<Properties3>, Properties4 extends Record<string, AnySchema>, Required4 extends StringKeys<Properties4>> (schema: { oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>, ObjectSchema<Properties4, Required4>] }, options?: any): Validator<{ oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>, ObjectSchema<Properties4, Required4>] }, ObjectFromSchema<Properties1, Required1> | ObjectFromSchema<Properties2, Required2> | ObjectFromSchema<Properties3, Required3> | ObjectFromSchema<Properties4, Required4>>
createFilter<Properties1 extends Record<string, AnySchema>, Required1 extends StringKeys<Properties1>, Properties2 extends Record<string, AnySchema>, Required2 extends StringKeys<Properties2>, Properties3 extends Record<string, AnySchema>, Required3 extends StringKeys<Properties3>, Properties4 extends Record<string, AnySchema>, Required4 extends StringKeys<Properties4>> (schema: { oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>, ObjectSchema<Properties4, Required4>] }, options?: any): Filter<{ oneOf: [ObjectSchema<Properties1, Required1>, ObjectSchema<Properties2, Required2>, ObjectSchema<Properties3, Required3>, ObjectSchema<Properties4, Required4>] }, ObjectFromSchema<Properties1, Required1> | ObjectFromSchema<Properties2, Required2> | ObjectFromSchema<Properties3, Required3> | ObjectFromSchema<Properties4, Required4>>

/* One of plain schema */
<Schema1 extends AnySchema, Schema2 extends AnySchema> (schema: { oneOf: [Schema1, Schema2] }, options?: any): Validator<{ oneOf: [Schema1, Schema2] }, TypeFromSchema<Schema1> | TypeFromSchema<Schema2>>
createFilter<Schema1 extends AnySchema, Schema2 extends AnySchema> (schema: { oneOf: [Schema1, Schema2] }, options?: any): Filter<{ oneOf: [Schema1, Schema2] }, TypeFromSchema<Schema1> | TypeFromSchema<Schema2>>
<Schema1 extends AnySchema, Schema2 extends AnySchema, Schema3 extends AnySchema> (schema: { oneOf: [Schema1, Schema2, Schema3] }, options?: any): Validator<{ oneOf: [Schema1, Schema2, Schema3] }, TypeFromSchema<Schema1> | TypeFromSchema<Schema2> | TypeFromSchema<Schema3>>
createFilter<Schema1 extends AnySchema, Schema2 extends AnySchema, Schema3 extends AnySchema> (schema: { oneOf: [Schema1, Schema2, Schema3] }, options?: any): Filter<{ oneOf: [Schema1, Schema2, Schema3] }, TypeFromSchema<Schema1> | TypeFromSchema<Schema2> | TypeFromSchema<Schema3>>
<Schema1 extends AnySchema, Schema2 extends AnySchema, Schema3 extends AnySchema, Schema4 extends AnySchema> (schema: { oneOf: [Schema1, Schema2, Schema3, Schema4] }, options?: any): Validator<{ oneOf: [Schema1, Schema2, Schema3, Schema4] }, TypeFromSchema<Schema1> | TypeFromSchema<Schema2> | TypeFromSchema<Schema3> | TypeFromSchema<Schema4>>
createFilter<Schema1 extends AnySchema, Schema2 extends AnySchema, Schema3 extends AnySchema, Schema4 extends AnySchema> (schema: { oneOf: [Schema1, Schema2, Schema3, Schema4] }, options?: any): Filter<{ oneOf: [Schema1, Schema2, Schema3, Schema4] }, TypeFromSchema<Schema1> | TypeFromSchema<Schema2> | TypeFromSchema<Schema3> | TypeFromSchema<Schema4>>

/* Object schema */
<Properties extends Record<string, AnySchema>, Required extends StringKeys<Properties>> (schema: ObjectSchema<Properties, Required>, options?: any): Validator<ObjectSchema<Properties, Required>>
createFilter<Properties extends Record<string, AnySchema>, Required extends StringKeys<Properties>> (schema: ObjectSchema<Properties, Required>, options?: any): Filter<ObjectSchema<Properties, Required>>

/* Plain schema */
<Schema extends AnySchema> (schema: Schema, options?: any): Validator<Schema>
createFilter<Schema extends AnySchema> (schema: Schema, options?: any): Filter<Schema>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to suggestions on the indentation here 馃槅

Opted to try and align the validator and filter as much as possible, to reduce the risk for them having subtle differences.

}

declare const factory: Factory
Expand Down
109 changes: 109 additions & 0 deletions test/typings.ts
Expand Up @@ -204,3 +204,112 @@ if (noRequiredFieldsValidator(input)) {
if (typeof input.b !== 'undefined') assertType<string>(input.b)
if (typeof input.c !== 'undefined') assertType<string>(input.c)
}

const animalValidator = createValidator({
oneOf: [
{
type: 'object',
properties: {
type: { enum: ['cat' as 'cat'] },
name: { type: 'string' }
},
required: [
'type',
'name'
]
},
{
type: 'object',
properties: {
type: { enum: ['dog' as 'dog'] },
name: { type: 'string' }
},
required: [
'type',
'name'
]
}
]
})

if (animalValidator(input)) {
if (input.type !== 'cat') assertType<'dog'>(input.type)
if (input.type !== 'dog') assertType<'cat'>(input.type)
assertType<string>(input.name)
}

const shapeValidator = createValidator({
oneOf: [
{ type: 'object', properties: { kind: { enum: ['triangle' as 'triangle'] } }, required: ['kind'] },
{ type: 'object', properties: { kind: { enum: ['rectangle' as 'rectangle'] } }, required: ['kind'] },
{ type: 'object', properties: { kind: { enum: ['circle' as 'circle'] } }, required: ['kind'] },
]
})

if (shapeValidator(input)) {
if (input.kind !== 'triangle' && input.kind !== 'rectangle') assertType<'circle'>(input.kind)
if (input.kind !== 'rectangle' && input.kind !== 'circle') assertType<'triangle'>(input.kind)
if (input.kind !== 'circle' && input.kind !== 'triangle') assertType<'rectangle'>(input.kind)
}

const foobar = createValidator({
oneOf: [
{ type: 'object', properties: { a: { type: 'string' } }, required: ['a'] },
{ type: 'object', properties: { b: { type: 'number' } }, required: ['b'] },
{ type: 'object', properties: { c: { type: 'boolean' } }, required: ['c'] },
{ type: 'object', properties: { d: { type: 'null' } }, required: ['d'] },
]
})

if (foobar(input)) {
if ('a' in input) assertType<string>(input.a)
if ('b' in input) assertType<number>(input.b)
if ('c' in input) assertType<boolean>(input.c)
if ('d' in input) assertType<null>(input.d)
}

const stringOrNullValidator = createValidator({
oneOf: [
{ type: 'string' },
{ type: 'null' }
]
})

if (stringOrNullValidator(input)) {
if (typeof input !== 'object') assertType<string>(input)
if (typeof input !== 'string') assertType<null>(input)
}

const primitiveValidator = createValidator({
oneOf: [
{ type: 'string' },
{ type: 'number' },
{ type: 'boolean' }
]
})

if (primitiveValidator(input)) {
if (typeof input !== 'string' && typeof input !== 'number') assertType<boolean>(input)
if (typeof input !== 'number' && typeof input !== 'boolean') assertType<string>(input)
if (typeof input !== 'boolean' && typeof input !== 'string') assertType<number>(input)
}

const overengineeredColorValidator = createValidator({
oneOf: [
{ enum: ['red' as 'red', 'pink' as 'pink'] },
{ enum: ['green' as 'green', 'olive' as 'olive'] },
{ enum: ['blue' as 'blue', 'teal' as 'teal'] },
{ enum: ['yellow' as 'yellow', 'cream' as 'cream'] }
]
})

if (overengineeredColorValidator(input)) {
if (input !== 'red' && input !== 'pink' && input !== 'green' && input !== 'olive' && input !== 'blue' && input !== 'teal' && input !== 'yellow') assertType<'cream'>(input)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe do a negated includes to make all of these a bit easier to read?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript won't be able to extract type information from that, unfortunately.

if (input !== 'pink' && input !== 'green' && input !== 'olive' && input !== 'blue' && input !== 'teal' && input !== 'yellow' && input !== 'cream') assertType<'red'>(input)
if (input !== 'green' && input !== 'olive' && input !== 'blue' && input !== 'teal' && input !== 'yellow' && input !== 'cream' && input !== 'red') assertType<'pink'>(input)
if (input !== 'olive' && input !== 'blue' && input !== 'teal' && input !== 'yellow' && input !== 'cream' && input !== 'red' && input !== 'pink') assertType<'green'>(input)
if (input !== 'blue' && input !== 'teal' && input !== 'yellow' && input !== 'cream' && input !== 'red' && input !== 'pink' && input !== 'green') assertType<'olive'>(input)
if (input !== 'teal' && input !== 'yellow' && input !== 'cream' && input !== 'red' && input !== 'pink' && input !== 'green' && input !== 'olive') assertType<'blue'>(input)
if (input !== 'yellow' && input !== 'cream' && input !== 'red' && input !== 'pink' && input !== 'green' && input !== 'olive' && input !== 'blue') assertType<'teal'>(input)
if (input !== 'cream' && input !== 'red' && input !== 'pink' && input !== 'green' && input !== 'olive' && input !== 'blue' && input !== 'teal') assertType<'yellow'>(input)
}