import { KeyOfAddString, KeyOfAddNumber, KeyOfAddStringOrUndefined, KeyOfAddNumberOrUndefined, KeyOfAddBoolean, KeyOfAddBooleanOrUndefined, KeyOfAddStringArray, KeyOfAddNumberArray, KeyOfAddBooleanArray, KeyOfAddCustomArray, KeyOfAddCustom, KeyOfAddCustomOrUndefined, KeyOfAddCustomArrayOrUndefined, KeyOfAddStringArrayOrUndefined, KeyOfAddNumberArrayOrUndefined, KeyOfAddBooleanArrayOrUndefined, KeyOfAddStringOrNull, KeyOfAddStringOrNullOrUndefined, KeyOfAddNumberOrNull, KeyOfAddNumberOrNullOrUndefined, KeyOfAddBooleanOrNull, KeyOfAddBooleanOrNullOrUndefined, KeyOfAddStringArrayOrNull, KeyOfAddStringArrayOrNullOrUndefined, KeyOfAddNumberArrayOrNull, KeyOfAddNumberArrayOrNullOrUndefined, KeyOfAddBooleanArrayOrNull, KeyOfAddBooleanArrayOrNullOrUndefined, KeyOfAddCustomOrNull, KeyOfAddCustomOrNullOrUndefined, KeyOfAddCustomArrayOrNull, KeyOfAddCustomArrayOrNullOrUndefined, } from "./keyTypers"; import { stringCaster, numberCaster, booleanCaster, wrapAnObjectCasterToSupportUndefined, stringOrUndefinedCaster, numberOrUndefinedCaster, booleanOrUndefinedCaster, wrapAnObjectCasterToSupportArray, stringArrayCaster, numberArrayCaster, booleanArrayCaster, stringArrayOrUndefinedCaster, numberArrayOrUndefinedCaster, booleanArrayOrUndefinedCaster, stringOrNullCaster, stringOrNullOrUndefinedCaster, numberOrNullCaster, numberOrNullOrUndefinedCaster, booleanOrNullCaster, booleanOrNullOrUndefinedCaster, stringArrayOrNullCaster, stringArrayOrNullOrUndefinedCaster, numberArrayOrNullCaster, numberArrayOrNullOrUndefinedCaster, booleanArrayOrNullCaster, booleanArrayOrNullOrUndefinedCaster, wrapAnObjectCasterToSupportNull, } from "./basicCasters"; import { WithoutNull, WithoutUndefined } from "./basicTypers"; import { Caster } from "./caster"; type CasterForArrayOrUndefined = T extends Array ? Caster> : never; type CasterForArrayOrNullOrUndefined = T extends Array ? Caster>> : never; type CasterForArrayOrNull = T extends Array ? Caster> : never; /** * Interface for a finished caster builder. */ interface ICasterBuilderReadyToBuild { build(): Caster; } /** * Partial casters type (casters for the already handled fields) * * Keys are limited, values for keys are not so limited (type for remaining key is enabled, but only hypothetically) */ type PartialCastersType = { [index in keyof Omit]: Caster; }; /** * Type depending on the remaining part: * - We can't build the final object when the remaining part is not empty * - We can build the final object when the remaining part is empty */ type CasterBuilderReturnType< TYPE extends REMAINING, REMAINING extends { /* EMPTY*/ }, > = keyof REMAINING extends never ? ICasterBuilderReadyToBuild : ICasterBuilderInProgress; /** * Interface for a caster builder in progress. */ interface ICasterBuilderInProgress< TYPE extends REMAINING, REMAINING extends { /* EMPTY */ }, > { /** * Add string field * * @param key Key of a string typed property */ addString( key: KeyOfAddString, ): CasterBuilderReturnType>; /** * Add string or undefined field * * @param key Key */ addStringOrUndefined( key: KeyOfAddStringOrUndefined, ): CasterBuilderReturnType>; /** * Add string or null field * * @param key Key */ addStringOrNull( key: KeyOfAddStringOrNull, ): CasterBuilderReturnType>; /** * Add string or null or undefined field * * @param key Key */ addStringOrNullOrUndefined( key: KeyOfAddStringOrNullOrUndefined, ): CasterBuilderReturnType>; /** * Add number field * * @param key Key */ addNumber( key: KeyOfAddNumber, ): CasterBuilderReturnType>; /** * Add number or undefined field * * @param key Key */ addNumberOrUndefined( key: KeyOfAddNumberOrUndefined, ): CasterBuilderReturnType>; /** * Add number or null field * * @param key Key */ addNumberOrNull( key: KeyOfAddNumberOrNull, ): CasterBuilderReturnType>; /** * Add number or null or undefined field * * @param key Key */ addNumberOrNullOrUndefined( key: KeyOfAddNumberOrNullOrUndefined, ): CasterBuilderReturnType>; /** * Add boolean field * * @param key Key */ addBoolean( key: KeyOfAddBoolean, ): CasterBuilderReturnType>; /** * Add boolean or undefined field * * @param key Key */ addBooleanOrUndefined( key: KeyOfAddBooleanOrUndefined, ): CasterBuilderReturnType>; /** * Add boolean or null field * * @param key Key */ addBooleanOrNull( key: KeyOfAddBooleanOrNull, ): CasterBuilderReturnType>; /** * Add boolean or null or undefined field * * @param key Key */ addBooleanOrNullOrUndefined( key: KeyOfAddBooleanOrNullOrUndefined, ): CasterBuilderReturnType>; /** * Add custom field * * @param key Key * @param caster Caster */ addCustom( key: KeyOfAddCustom, caster: Caster, ): CasterBuilderReturnType>; /** * Add custom or undefined field * * @param key Key * @param caster Caster */ addCustomOrUndefined( key: KeyOfAddCustomOrUndefined, caster: Caster>, ): CasterBuilderReturnType>; /** * Add custom or null field * * @param key Key * @param caster Caster */ addCustomOrNull( key: KeyOfAddCustomOrNull, caster: Caster>, ): CasterBuilderReturnType>; /** * Add custom or null or undefined field * * @param key Key * @param caster Caster */ addCustomOrNullOrUndefined( key: KeyOfAddCustomOrNullOrUndefined, caster: Caster>>, ): CasterBuilderReturnType>; /** * Add string array field * * @param key Key */ addStringArray( key: KeyOfAddStringArray, ): CasterBuilderReturnType>; /** * Add string array or undefined field * * @param key Key */ addStringArrayOrUndefined( key: KeyOfAddStringArrayOrUndefined, ): CasterBuilderReturnType>; /** * Add string array or null field * * @param key Key */ addStringArrayOrNull( key: KeyOfAddStringArrayOrNull, ): CasterBuilderReturnType>; /** * Add string array or null or undefined field * * @param key Key */ addStringArrayOrNullOrUndefined( key: KeyOfAddStringArrayOrNullOrUndefined, ): CasterBuilderReturnType>; /** * Add number array field * * @param key Key */ addNumberArray( key: KeyOfAddNumberArray, ): CasterBuilderReturnType>; /** * Add number array or undefined field * * @param key Key */ addNumberArrayOrUndefined( key: KeyOfAddNumberArrayOrUndefined, ): CasterBuilderReturnType>; /** * Add number array or null field * * @param key Key */ addNumberArrayOrNull( key: KeyOfAddNumberArrayOrNull, ): CasterBuilderReturnType>; /** * Add number array or null or undefined field * * @param key Key */ addNumberArrayOrNullOrUndefined( key: KeyOfAddNumberArrayOrNullOrUndefined, ): CasterBuilderReturnType>; /** * Add boolean array field * * @param key Key */ addBooleanArray( key: KeyOfAddBooleanArray, ): CasterBuilderReturnType>; /** * Add boolean array or undefined field * * @param key Key */ addBooleanArrayOrUndefined( key: KeyOfAddBooleanArrayOrUndefined, ): CasterBuilderReturnType>; /** * Add boolean array or null field * * @param key Key */ addBooleanArrayOrNull( key: KeyOfAddBooleanArrayOrNull, ): CasterBuilderReturnType>; /** * Add boolean array or null or undefined field * * @param key Key */ addBooleanArrayOrNullOrUndefined( key: KeyOfAddBooleanArrayOrNullOrUndefined, ): CasterBuilderReturnType>; /** * Add custom field * * @param key Key * @param caster Caster */ addCustomArray( key: KeyOfAddCustomArray, caster: TYPE[KEY] extends Array ? Caster : never, ): CasterBuilderReturnType>; addCustomArrayOrUndefined( key: KeyOfAddCustomArrayOrUndefined, caster: CasterForArrayOrUndefined, ): CasterBuilderReturnType>; addCustomArrayOrNull( key: KeyOfAddCustomArrayOrNull, caster: CasterForArrayOrNull, ): CasterBuilderReturnType>; addCustomArrayOrNullOrUndefined( key: KeyOfAddCustomArrayOrNullOrUndefined, caster: CasterForArrayOrNullOrUndefined, ): CasterBuilderReturnType>; } /** * Build a given caster from a partial caster object; * * @param partialCasters Partial casters */ const buildCaster = < TYPE extends REMAINING, REMAINING extends { /* EMPTY*/ }, >( partialCasters: PartialCastersType, ): Caster => { const doSync = ( value: unknown, callCaster: ( caster: PartialCastersType[Exclude], value: unknown, key: Exclude, ) => void, ): void => { if (typeof value !== "object" || value === null) { throw new Error("The value is not an object."); } const keys = Object.keys(partialCasters) as Array>; keys.forEach((key) => { const caster = partialCasters[key]; const typedValue = value as { [index in keyof TYPE]: TYPE[index] }; try { callCaster(caster, typedValue[key], key); } catch (error) { throw new Error(`${String(key)} -> ${(error as Error).message}`); } }); }; const doAsync = ( value: unknown, callCaster: ( caster: PartialCastersType[Exclude], value: unknown, key: Exclude, ) => Promise, ): Array> => { if (typeof value !== "object" || value === null) { throw new Error("The value is not an object."); } const keys = Object.keys(partialCasters) as Array>; const promises = keys.map(async (key) => { const caster = partialCasters[key]; const typedValue = value as { [index in keyof TYPE]: TYPE[index] }; try { await callCaster(caster, typedValue[key], key); } catch (error) { throw new Error(`${String(key)} -> ${(error as Error).message}`); } }); return promises; }; const CasterClass = class extends Caster { public readonly isType = (value: unknown): value is TYPE => { if (typeof value !== "object" || value === null) { return false; } let result = true; const keys = Object.keys(partialCasters) as Array>; keys.forEach((key) => { const caster = partialCasters[key]; const typedValue = value as { [index in keyof TYPE]: TYPE[index] }; if (!caster.isType(typedValue[key])) { result = false; } }); return result; }; public readonly castTo = (value: unknown): TYPE => { doSync(value, (c, v) => { c.castTo(v); }); return value as TYPE; }; public readonly copyTo = (value: unknown): TYPE => { const result: { [index in keyof TYPE]?: TYPE[index] } = {}; doSync(value, (c, v, k) => { result[k] = c.copyTo(v); }); return result as TYPE; }; public readonly asyncCastTo = async (value: unknown): Promise => { const promises = await doAsync(value, async (c, v) => { await c.asyncCastTo(v); }); await Promise.all(promises); return value as TYPE; }; public readonly asyncCopyTo = async (value: unknown): Promise => { const result: { [index in keyof TYPE]?: TYPE[index] } = {}; const promises = doAsync(value, async (c, v, k) => { result[k] = await c.asyncCopyTo(v); }); await Promise.all(promises); return result as TYPE; }; }; return new CasterClass(); }; class CasterBuilder< TYPE extends REMAINING, REMAINING extends { /* EMPTY*/ }, > implements ICasterBuilderReadyToBuild, ICasterBuilderInProgress { /** * The current casters we have to build the final solution. */ private partialCasters: PartialCastersType; /** * Just a good old friendly private constructor. Nothing to see here. * Please use the `createInstance` method to create new instances! Thanks! * * @param partialCasters current casters */ private constructor(partialCasters: PartialCastersType) { this.partialCasters = partialCasters; } /** * Static function to create a new instance (instead of constructor. Constructors are evil.) */ public static createInstance = < INSTANCE_TYPE extends { /* EMPTY*/ }, >(): CasterBuilder => { return new CasterBuilder({}); }; private static updatePartialCasters = < TYPE extends REMAINING, REMAINING extends { /* EMPTY*/ }, KEY extends keyof TYPE, >( partialCasters: PartialCastersType, key: keyof TYPE, caster: Caster, // TODO: This is not so great... Maybe we could do this better. ): CasterBuilderReturnType> => { const newPartialCasters = { ...partialCasters, [key]: caster, } as unknown as PartialCastersType>; return new CasterBuilder>(newPartialCasters) as unknown as CasterBuilderReturnType< TYPE, Omit >; }; /** * Add a string checker to a key. * * @param key Key of a string typed property */ public readonly addString = ( key: KeyOfAddString, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringCaster); }; /** * Add a string or undefined checker to a key. */ public readonly addStringOrUndefined = ( key: KeyOfAddStringOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringOrUndefinedCaster); }; /** * Add a string or null checker to a key. */ public readonly addStringOrNull = ( key: KeyOfAddStringOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringOrNullCaster); }; /** * Add a string or null or undefined checker to a key. */ public readonly addStringOrNullOrUndefined = ( key: KeyOfAddStringOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringOrNullOrUndefinedCaster); }; /** * Add a number checker to a key. */ public readonly addNumber = ( key: KeyOfAddNumber, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberCaster); }; /** * Add a number or undefined checker to a key. */ public readonly addNumberOrUndefined = ( key: KeyOfAddNumberOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberOrUndefinedCaster); }; /** * Add a number or null checker to a key. */ public readonly addNumberOrNull = ( key: KeyOfAddNumberOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberOrNullCaster); }; /** * Add a number or null or undefined checker to a key. */ public readonly addNumberOrNullOrUndefined = ( key: KeyOfAddNumberOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberOrNullOrUndefinedCaster); }; /** * Add a boolean checker to a key. */ public readonly addBoolean = ( key: KeyOfAddBoolean, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanCaster); }; /** * Add a boolean or undefined checker to a key. */ public readonly addBooleanOrUndefined = ( key: KeyOfAddBooleanOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanOrUndefinedCaster); }; /** * Add a boolean or null checker to a key. */ public readonly addBooleanOrNull = ( key: KeyOfAddBooleanOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanOrNullCaster); }; /** * Add a boolean or null or undefined checker to a key. */ public readonly addBooleanOrNullOrUndefined = ( key: KeyOfAddBooleanOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanOrNullOrUndefinedCaster); }; /** * You can't have caster for a non-compatible type. Or can you have? */ public readonly addCustom = ( key: KeyOfAddCustom, caster: Caster>, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, caster); }; public readonly addCustomOrUndefined = ( key: KeyOfAddCustomOrUndefined, caster: Caster>, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters( this.partialCasters, key, wrapAnObjectCasterToSupportUndefined(caster), ); }; public readonly addCustomOrNull = ( key: KeyOfAddCustomOrNull, caster: Caster>, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, wrapAnObjectCasterToSupportNull(caster)); }; public readonly addCustomOrNullOrUndefined = ( key: KeyOfAddCustomOrNullOrUndefined, caster: Caster>>, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters( this.partialCasters, key, wrapAnObjectCasterToSupportNull(wrapAnObjectCasterToSupportUndefined(caster)), ); }; public readonly addStringArray = ( key: KeyOfAddStringArray, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringArrayCaster); }; public readonly addStringArrayOrUndefined = ( key: KeyOfAddStringArrayOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringArrayOrUndefinedCaster); }; public readonly addStringArrayOrNull = ( key: KeyOfAddStringArrayOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringArrayOrNullCaster); }; public readonly addStringArrayOrNullOrUndefined = ( key: KeyOfAddStringArrayOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, stringArrayOrNullOrUndefinedCaster); }; public readonly addNumberArray = ( key: KeyOfAddNumberArray, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberArrayCaster); }; public readonly addNumberArrayOrUndefined = ( key: KeyOfAddNumberArrayOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberArrayOrUndefinedCaster); }; public readonly addNumberArrayOrNull = ( key: KeyOfAddNumberArrayOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberArrayOrNullCaster); }; public readonly addNumberArrayOrNullOrUndefined = ( key: KeyOfAddNumberArrayOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, numberArrayOrNullOrUndefinedCaster); }; public readonly addBooleanArray = ( key: KeyOfAddBooleanArray, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanArrayCaster); }; public readonly addBooleanArrayOrUndefined = ( key: KeyOfAddBooleanArrayOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanArrayOrUndefinedCaster); }; public readonly addBooleanArrayOrNull = ( key: KeyOfAddBooleanArrayOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanArrayOrNullCaster); }; public readonly addBooleanArrayOrNullOrUndefined = ( key: KeyOfAddBooleanArrayOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, booleanArrayOrNullOrUndefinedCaster); }; public readonly addCustomArray = ( key: KeyOfAddCustomArray, caster: TYPE[KEY] extends Array ? Caster> : never, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters(this.partialCasters, key, wrapAnObjectCasterToSupportArray(caster)); }; public readonly addCustomArrayOrUndefined = ( key: KeyOfAddCustomArrayOrUndefined, caster: CasterForArrayOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters( this.partialCasters, key, wrapAnObjectCasterToSupportUndefined(wrapAnObjectCasterToSupportArray(caster)), ); }; public readonly addCustomArrayOrNull = ( key: KeyOfAddCustomArrayOrNull, caster: CasterForArrayOrNull, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters( this.partialCasters, key, wrapAnObjectCasterToSupportNull(wrapAnObjectCasterToSupportArray(caster)), ); }; public readonly addCustomArrayOrNullOrUndefined = ( key: KeyOfAddCustomArrayOrNullOrUndefined, caster: CasterForArrayOrNullOrUndefined, ): CasterBuilderReturnType> => { return CasterBuilder.updatePartialCasters( this.partialCasters, key, wrapAnObjectCasterToSupportNull( wrapAnObjectCasterToSupportUndefined(wrapAnObjectCasterToSupportArray(caster)), ), ); }; /** * Build the final type checker. */ public readonly build = (): Caster => { return buildCaster(this.partialCasters); }; } /** * Create a new caster builder. * * The default value is `never` just to be sure that there will be a value for the `TYPE`. */ export function createCaster< TYPE extends { /* EMPTY*/ } = never, >(): CasterBuilderReturnType { const newCasterInstance: CasterBuilder = CasterBuilder.createInstance(); return newCasterInstance as unknown as CasterBuilderReturnType; }