Skip to content

Commit

Permalink
Merge pull request #5 from Resetand/validate-type-improvements
Browse files Browse the repository at this point in the history
improve type inference for validate
  • Loading branch information
Resetand authored Mar 3, 2024
2 parents 7e44591 + c783902 commit 8110f99
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 69 deletions.
59 changes: 26 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -540,39 +540,6 @@ isEmptyArray([1, 2, 3]); // false
Allows to validate runtime values (objects) with given schema or guard

### `validate` function args

One of the use cases for `validate` is to validate runtime values with given schema or guard

```ts
type FunctionExample = {
(value: string): string;
(value: string, otherValue: number): string;
(value: string, otherValue: number[]): string;
};

const example: FunctionExample = (...args: unknown[]) => {
if (validate(args, tuple(isString))) {
const [value] = args; // [string]
}
if (validate(args, tuple(isString, isNumber))) {
const [value, otherValue] = args; // [string, number]
}
if (validate(args, tuple(isString, isArrayOf(isNumber)))) {
const [value, otherValue] = args; // [string, number[]]
}

// fallback
};

/**
* This hack is required to correct type inference
* Although typescript v5+ has `const` genetic modifier, that allows to infer such cases correctly
* most of the projects use older versions of typescript, and this feature is breaking declaration files
*/
const tuple = <T extends unknown[]>(...args: T) => args;
```

### Usage

#### Validate object with schema
Expand Down Expand Up @@ -628,6 +595,32 @@ if (validate(arr, schema)) {
}
```

#### Validate function args

One of the useful use-cases is to validate overloaded function arguments

```ts
type FunctionExample = {
(value: string): void;
(value: string, otherValue: number): void;
(value: string, otherValue: number[]): void;
};

const example: FunctionExample = (...args: unknown[]) => {
if (validate(args, [isString])) {
const [value] = args; // [string]
}
if (validate(args, [isString, isNumber])) {
const [value, otherValue] = args; // [string, number]
}
if (validate(args, [isString, isArrayOf(isNumber)])) {
const [value, otherValue] = args; // [string, number[]]
}

// fallback
};
```

#### Validate value with guard

```tsx
Expand Down
3 changes: 2 additions & 1 deletion src/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type InferTypeSchema<TSchema> = TSchema extends unknown[]
? { [K in keyof TSchema]: InferTypeSchema<TSchema[K]> }
: InferGuardType<TSchema>;

export type TypeSchema = { [key: PropertyKey]: TypeSchema } | TypeSchema[] | Guard;
type ObjectSchema = { [K in PropertyKey]: TypeSchema };
export type TypeSchema = ObjectSchema | Guard | [] | [TypeSchema] | [TypeSchema, ...TypeSchema[]];

export type InferGuardType<TGuard> = TGuard extends Guard<infer TGuarded, any[]> ? TGuarded : never;
9 changes: 7 additions & 2 deletions src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import isHas from './guards/isHas';
import isArray from './guards/isArray';

type ValidateGuard = {
<TSchema extends TypeSchema>(value: unknown, schema: TSchema): value is InferTypeSchema<TSchema>;
<TSchema extends TypeSchema>(schema: TSchema): (value: unknown) => value is InferTypeSchema<TSchema>;
<TSchema extends TypeSchema>(
value: unknown,
schema: TSchema | Readonly<TSchema>,
): value is InferTypeSchema<TSchema>;
<TSchema extends TypeSchema>(schema: TSchema | Readonly<TSchema>): (
value: unknown,
) => value is InferTypeSchema<TSchema>;
};

const validateFactory = (options: { strict: boolean }) => {
Expand Down
132 changes: 99 additions & 33 deletions tests/validate.test.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,110 @@
import { test, expect } from 'vitest';
import { test, expect, describe } from 'vitest';
import is, { validate, validateStrict } from '../src';
import { assertGuardedType } from './utils';

test('Should validate by schema shape (validate)', () => {
const obj = {
a: 1,
b: 'string',
c: {
d: true,
e: {
f: 1,
g: 'string',
describe('Validate runtime tests', () => {
test('Should validate by schema shape (validate)', () => {
const obj = {
a: 1,
b: 'string',
c: {
d: true,
e: {
f: 1,
g: 'string',
},
},
},
};

const schema = {
a: is.Number,
b: is.$some(is.String, is.Nil),
c: {
d: is.Boolean,
e: {
f: is.Number,
g: is.String,
};

const schema = {
a: is.Number,
b: is.$some(is.String, is.Nil),
c: {
d: is.Boolean,
e: {
f: is.Number,
g: is.String,
},
},
},
};
};

expect(validate(obj, schema)).toBe(true);

expect(validate(42, is.Number)).toBe(true);
expect(validate(42, is.$some(is.Number, is.Nil))).toBe(true);

expect(validate(42, is.String)).toBe(false);
expect(validate({ a: 1 }, schema)).toBe(false);
expect(validate('SOME STRING VALUE', schema)).toBe(false);

expect(validate({ a: 1, otherParam: 'here' }, { a: is.Number })).toBe(true);
expect(validateStrict({ a: 1, otherParam: 'here' }, { a: is.Number })).toBe(false);

expect(validate([21, 2, 32], is.ArrayOf(is.Number))).toBe(true);
expect(validate(is.ArrayOf(is.Number))([21, 2, 32])).toBe(true);

expect(() => validate(42, 'UNKNOWN_SCHEMA' as any)).toThrowError();
});

expect(validate(obj, schema)).toBe(true);
test('Should validate array schema', () => {
const schema = [is.Number, is.String, is.Boolean];

expect(validate([1, '2', true], schema)).toBe(true);
expect(validate([1, '2', true, 4], schema)).toBe(false);
expect(validate([1, '2'], schema)).toBe(false);
expect(validate([1, '2', 'true'], schema)).toBe(false);
});

test('Should validate array schema (complex type)', () => {
const schema = [is.Number, is.String, is.Boolean, [validate({ a: is.Number, b: is.String })]];

expect(validate([1, '2', true, [{ a: 1, b: '2' }]], schema)).toBe(true);
expect(validate([1, '2', true, [{ a: 1, b: 2 }]], schema)).toBe(false);
expect(validate([1, '2', true, [{ a: 1 }]], schema)).toBe(false);
expect(validate([1, '2', true, [{ a: 1, b: '2', c: 3 }]], schema)).toBe(true); // extra properties are allowed
});
});

describe('Validate static typing tests', () => {
test('should infer guarded type from object schema', () => {
const schema = {
a: is.Number,
b: is.$some(is.String, is.Nil),
c: {
d: is.Boolean,
e: {
f: is.Number,
g: is.String,
},
},
};

expect(validate(42, is.Number)).toBe(true);
expect(validate(42, is.$some(is.Number, is.Nil))).toBe(true);
type ExpectedType = {
a: number;
b: string | null | undefined;
c: {
d: boolean;
e: {
f: number;
g: string;
};
};
};

expect(validate(42, is.String)).toBe(false);
expect(validate({ a: 1 }, schema)).toBe(false);
assertGuardedType<ExpectedType>()(validate(schema));
assertGuardedType<ExpectedType>()(validateStrict(schema));
});

expect(validate({ a: 1, otherParam: 'here' }, { a: is.Number })).toBe(true);
expect(validateStrict({ a: 1, otherParam: 'here' }, { a: is.Number })).toBe(false);
test('should infer guarded type from array schema', () => {
type ExpectedType = [number, string, boolean];

expect(validate([21, 2, 32], is.ArrayOf(is.Number))).toBe(true);
expect(validate(is.ArrayOf(is.Number))([21, 2, 32])).toBe(true);
assertGuardedType<ExpectedType>()(validate([is.Number, is.String, is.Boolean]), []);
assertGuardedType<ExpectedType>()(validateStrict([is.Number, is.String, is.Boolean]), []);
});

expect(() => validate(42, 'UNKNOWN_SCHEMA' as any)).toThrowError();
test('should infer guarded type from guard schema', () => {
assertGuardedType<number>()(validate(is.Number));
assertGuardedType<string>()(validate(is.String));
assertGuardedType<string | null | undefined>()(validate(is.$some(is.String, is.Nil)));
});
});

0 comments on commit 8110f99

Please sign in to comment.