From b5f2cbafd6cddd659da502267f69cf6a5491ca78 Mon Sep 17 00:00:00 2001 From: ASafaierad Date: Wed, 27 Dec 2023 03:04:14 +0100 Subject: [PATCH] fix: fix nested object schema type inference --- src/Config.spec.ts | 39 +++++++++++++++++++++++++++++++++++++ src/Config.ts | 14 ++++++------- src/InferType.ts | 13 ------------- src/Schema/BooleanSchema.ts | 6 ++++-- src/Schema/NumberSchema.ts | 2 ++ src/Schema/ObjectSchema.ts | 2 ++ src/Schema/StringSchema.ts | 2 ++ src/index.ts | 2 +- src/types.ts | 32 ++++++++++++++++++++++++++++++ 9 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 src/Config.spec.ts delete mode 100644 src/InferType.ts create mode 100644 src/types.ts diff --git a/src/Config.spec.ts b/src/Config.spec.ts new file mode 100644 index 0000000..bcb5576 --- /dev/null +++ b/src/Config.spec.ts @@ -0,0 +1,39 @@ +import { Config } from './Config'; +import type { Equals, Expect } from './types'; + +describe('Config', () => { + it('should parse nested object', () => { + const config = new Config({ + s: Config.string(), + n: Config.number().require(), + foo: Config.object({ + foo1: Config.string().require(), + foo2: Config.object({ foo3: Config.boolean() }), + }), + }) + .parse({ + s: 's', + n: 0, + foo: { foo1: 'foo1', foo2: { foo3: false } }, + }) + .getAll(); + + expect(config).toEqual({ + s: 's', + n: 0, + foo: { foo1: 'foo1', foo2: { foo3: false } }, + }); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type Test = Expect< + Equals< + typeof config, + { + s: string | undefined; + n: number; + foo: { foo1: string; foo2: { foo3: boolean | undefined } }; + } + > + >; + }); +}); diff --git a/src/Config.ts b/src/Config.ts index 83c1615..956b6fb 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -1,13 +1,11 @@ -import type { InferType } from './InferType'; import type { Schema, SchemaOptions } from './Schema'; import { BooleanSchema, NumberSchema, StringSchema } from './Schema'; import { ObjectSchema } from './Schema/ObjectSchema'; import type { SchemaWithDefaultOptions } from './Schema/SchemaOptions'; - -type RequiredSchema = T & { isRequired: true }; +import type { InferSchema, Prettify, RequiredSchema } from './types'; export class Config> { - private value!: InferType; + private value!: InferSchema; constructor(private schema: TSchema) {} @@ -18,7 +16,7 @@ export class Config> { s.key = key; s.setValue(this.value[key]); s.validate(); - this.value[key as keyof InferType] = s.parse(); + this.value[key as keyof InferSchema] = s.parse(); }); return this; @@ -55,10 +53,10 @@ export class Config> { } public get(key: TKey) { - return this.value[key] as InferType[TKey]; + return this.value[key] as Prettify[TKey]>; } - public getAll(): InferType { - return this.value as any; + public getAll() { + return this.value as Prettify>; } } diff --git a/src/InferType.ts b/src/InferType.ts deleted file mode 100644 index 71fe4cf..0000000 --- a/src/InferType.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Schema } from './Schema'; - -type Prettify = { - [K in keyof T]: T[K]; -} & {}; // eslint-disable-line @typescript-eslint/ban-types - -export type InferType> = { - [K in keyof T]: T[K] extends Schema - ? Prettify> - : T[K]['isRequired'] extends true - ? NonNullable - : T[K]['value']; -}; diff --git a/src/Schema/BooleanSchema.ts b/src/Schema/BooleanSchema.ts index 7716a27..610e9e9 100644 --- a/src/Schema/BooleanSchema.ts +++ b/src/Schema/BooleanSchema.ts @@ -2,12 +2,14 @@ import { Schema } from './Schema'; import type { SchemaOptions } from './SchemaOptions'; export class BooleanSchema extends Schema { - private falseRegex = /false/i; + #type = 'boolean'; // eslint-disable-line no-unused-private-class-members + #falseRegex = /false/i; + constructor(options: SchemaOptions = {}) { super({ ...options, typeConstructor: n => { - if (typeof n === 'string' && this.falseRegex.test(n)) return false; + if (typeof n === 'string' && this.#falseRegex.test(n)) return false; return Boolean(n); }, diff --git a/src/Schema/NumberSchema.ts b/src/Schema/NumberSchema.ts index b71a08f..3e0f790 100644 --- a/src/Schema/NumberSchema.ts +++ b/src/Schema/NumberSchema.ts @@ -25,6 +25,8 @@ class MaxNumberGuard implements Guard { } export class NumberSchema extends Schema { + #type = 'number'; // eslint-disable-line no-unused-private-class-members + constructor(options: SchemaOptions = {}) { super({ ...options, typeConstructor: Number, type: 'number' }); } diff --git a/src/Schema/ObjectSchema.ts b/src/Schema/ObjectSchema.ts index e23bd08..b6b2021 100644 --- a/src/Schema/ObjectSchema.ts +++ b/src/Schema/ObjectSchema.ts @@ -17,6 +17,8 @@ class ObjectGuard implements Guard> { } export class ObjectSchema extends Schema { + #type = 'object'; // eslint-disable-line no-unused-private-class-members + constructor(schema: Record) { super({ typeConstructor: x => x, diff --git a/src/Schema/StringSchema.ts b/src/Schema/StringSchema.ts index f17c633..7dbab9c 100644 --- a/src/Schema/StringSchema.ts +++ b/src/Schema/StringSchema.ts @@ -2,6 +2,8 @@ import { Schema } from './Schema'; import type { SchemaOptions } from './SchemaOptions'; export class StringSchema extends Schema { + #type = 'string'; // eslint-disable-line no-unused-private-class-members + constructor(options: SchemaOptions = {}) { super({ ...options, typeConstructor: String, type: 'string' }); } diff --git a/src/index.ts b/src/index.ts index cf791e8..4aba2da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export * from './Config'; -export type { InferType } from './InferType'; +export type { InferSchema } from './types'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..68fdf40 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,32 @@ +import type { Schema, StringSchema } from './Schema'; +import type { ObjectSchema } from './Schema/ObjectSchema'; + +export type Prettify = { + [K in keyof T]: T[K]; +} & {}; // eslint-disable-line @typescript-eslint/ban-types + +export type InferObjectSchema = T extends ObjectSchema< + infer G +> + ? G extends Record + ? Prettify> + : never + : never; + +export type InferSchema> = { + [K in keyof T]: T[K] extends ObjectSchema + ? InferObjectSchema + : T[K]['isRequired'] extends true + ? NonNullable + : T[K]['value']; +}; + +export type Expect = T; + +export type Equals = (() => T extends X ? 1 : 2) extends < + T, +>() => T extends Y ? 1 : 2 + ? true + : false; + +export type RequiredSchema = T & { isRequired: true };