From c783902f74342cd74a008176c1107571f8090d32 Mon Sep 17 00:00:00 2001 From: Kirill Machekhin Date: Mon, 4 Mar 2024 00:39:53 +0400 Subject: [PATCH] improve type inferince for validate --- README.md | 59 ++++++++---------- src/_types.ts | 3 +- src/validate.ts | 9 ++- tests/validate.test.ts | 132 ++++++++++++++++++++++++++++++----------- 4 files changed, 134 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index fd8ecdc..e0e8dc8 100644 --- a/README.md +++ b/README.md @@ -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 = (...args: T) => args; -``` - ### Usage #### Validate object with schema @@ -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 diff --git a/src/_types.ts b/src/_types.ts index aa78848..58fcbcc 100644 --- a/src/_types.ts +++ b/src/_types.ts @@ -41,6 +41,7 @@ export type InferTypeSchema = TSchema extends unknown[] ? { [K in keyof TSchema]: InferTypeSchema } : InferGuardType; -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 extends Guard ? TGuarded : never; diff --git a/src/validate.ts b/src/validate.ts index 86a4ef8..37d14e5 100644 --- a/src/validate.ts +++ b/src/validate.ts @@ -7,8 +7,13 @@ import isHas from './guards/isHas'; import isArray from './guards/isArray'; type ValidateGuard = { - (value: unknown, schema: TSchema): value is InferTypeSchema; - (schema: TSchema): (value: unknown) => value is InferTypeSchema; + ( + value: unknown, + schema: TSchema | Readonly, + ): value is InferTypeSchema; + (schema: TSchema | Readonly): ( + value: unknown, + ) => value is InferTypeSchema; }; const validateFactory = (options: { strict: boolean }) => { diff --git a/tests/validate.test.ts b/tests/validate.test.ts index 184b162..f188936 100644 --- a/tests/validate.test.ts +++ b/tests/validate.test.ts @@ -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()(validate(schema)); + assertGuardedType()(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()(validate([is.Number, is.String, is.Boolean]), []); + assertGuardedType()(validateStrict([is.Number, is.String, is.Boolean]), []); + }); - expect(() => validate(42, 'UNKNOWN_SCHEMA' as any)).toThrowError(); + test('should infer guarded type from guard schema', () => { + assertGuardedType()(validate(is.Number)); + assertGuardedType()(validate(is.String)); + assertGuardedType()(validate(is.$some(is.String, is.Nil))); + }); });