diff --git a/index.d.ts b/index.d.ts index 5e82250ff..a9cb477eb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -66,7 +66,7 @@ export type {SimplifyDeep} from './source/simplify-deep'; export type {Jsonify} from './source/jsonify'; export type {Jsonifiable} from './source/jsonifiable'; export type {StructuredCloneable} from './source/structured-cloneable'; -export type {Schema} from './source/schema'; +export type {Schema, SchemaOptions} from './source/schema'; export type {LiteralToPrimitive} from './source/literal-to-primitive'; export type {LiteralToPrimitiveDeep} from './source/literal-to-primitive-deep'; export type { diff --git a/source/schema.d.ts b/source/schema.d.ts index 2730f8e7e..0c38f43be 100644 --- a/source/schema.d.ts +++ b/source/schema.d.ts @@ -19,6 +19,7 @@ interface User { created: Date; active: boolean; passwordHash: string; + attributes: ['Foo', 'Bar'] } type UserMask = Schema; @@ -32,12 +33,13 @@ const userMaskSettings: UserMask = { created: 'show', active: 'show', passwordHash: 'hide', + attributes: ['mask', 'show'] } ``` @category Object */ -export type Schema = ObjectType extends string +export type Schema = ObjectType extends string ? ValueType : ObjectType extends Map ? ValueType @@ -48,7 +50,9 @@ export type Schema = ObjectType extends string : ObjectType extends ReadonlySet ? ValueType : ObjectType extends Array - ? Array> + ? Options['recurseIntoArrays'] extends false | undefined + ? ValueType + : Array> : ObjectType extends (...arguments_: unknown[]) => unknown ? ValueType : ObjectType extends Date @@ -58,14 +62,53 @@ export type Schema = ObjectType extends string : ObjectType extends RegExp ? ValueType : ObjectType extends object - ? SchemaObject + ? SchemaObject : ValueType; /** Same as `Schema`, but accepts only `object`s as inputs. Internal helper for `Schema`. */ -type SchemaObject = { - [KeyType in keyof ObjectType]: ObjectType[KeyType] extends readonly unknown[] | unknown[] - ? Schema - : Schema | K; +type SchemaObject< + ObjectType extends object, + K, + Options extends SchemaOptions, +> = { + [KeyType in keyof ObjectType]: ObjectType[KeyType] extends + | readonly unknown[] + | unknown[] + ? Options['recurseIntoArrays'] extends false | undefined + ? K + : Schema + : Schema | K; +}; + +/** +@see Schema +*/ +export type SchemaOptions = { + /** + By default, this affects elements in array and tuple types. You can change this by passing `{recurseIntoArrays: false}` as the third type argument: + - If `recurseIntoArrays` is set to `true` (default), array elements will be recursively processed as well. + - If `recurseIntoArrays` is set to `false`, arrays will not be recursively processed, and the entire array will be replaced with the given value type. + + @example + ``` + type UserMask = Schema; + + const userMaskSettings: UserMask = { + id: 'show', + name: { + firstname: 'show', + lastname: 'mask', + }, + created: 'show', + active: 'show', + passwordHash: 'hide', + attributes: 'hide' + } + ``` + + @default true + */ + readonly recurseIntoArrays?: boolean | undefined; }; diff --git a/test-d/schema.ts b/test-d/schema.ts index 8bcb44ba7..e30ded0a4 100644 --- a/test-d/schema.ts +++ b/test-d/schema.ts @@ -126,3 +126,77 @@ expectType(complexBarSchema.readonlySet); expectType(complexBarSchema.readonlyArray); expectType(complexBarSchema.readonlyTuple); expectType(complexBarSchema.regExp); + +// With Options and `recurseIntoArrays` set to `false` +type FooSchemaWithOptionsNoRecurse = Schema; + +const fooSchemaWithOptionsNoRecurse: FooSchemaWithOptionsNoRecurse = { + baz: 'A', + bar: { + function: 'A', + object: {key: 'A'}, + string: 'A', + number: 'A', + boolean: 'A', + symbol: 'A', + map: 'A', + set: 'A', + array: 'A', + tuple: 'A', + objectArray: 'A', + readonlyMap: 'A', + readonlySet: 'A', + readonlyArray: 'A' as const, + readonlyTuple: 'A' as const, + regExp: 'A', + }, +}; + +expectNotAssignable(foo); +expectNotAssignable({key: 'value'}); +expectNotAssignable(new Date()); +expectType(fooSchemaWithOptionsNoRecurse.baz); + +const barSchemaWithOptionsNoRecurse = fooSchemaWithOptionsNoRecurse.bar as Schema; +expectType(barSchemaWithOptionsNoRecurse.function); +expectType(barSchemaWithOptionsNoRecurse.object); +expectType(barSchemaWithOptionsNoRecurse.string); +expectType(barSchemaWithOptionsNoRecurse.number); +expectType(barSchemaWithOptionsNoRecurse.boolean); +expectType(barSchemaWithOptionsNoRecurse.symbol); +expectType(barSchemaWithOptionsNoRecurse.map); +expectType(barSchemaWithOptionsNoRecurse.set); +expectType(barSchemaWithOptionsNoRecurse.array); +expectType(barSchemaWithOptionsNoRecurse.tuple); +expectType(barSchemaWithOptionsNoRecurse.objectArray); +expectType(barSchemaWithOptionsNoRecurse.readonlyMap); +expectType(barSchemaWithOptionsNoRecurse.readonlySet); +expectType(barSchemaWithOptionsNoRecurse.readonlyArray); +expectType(barSchemaWithOptionsNoRecurse.readonlyTuple); +expectType(barSchemaWithOptionsNoRecurse.regExp); + +// With Options and `recurseIntoArrays` set to `true` +type FooSchemaWithOptionsRecurse = Schema; + +expectNotAssignable(foo); +expectNotAssignable({key: 'value'}); +expectNotAssignable(new Date()); +expectType(fooSchema.baz); + +const barSchemaWithOptionsRecurse = fooSchema.bar as Schema; +expectType(barSchemaWithOptionsRecurse.function); +expectType(barSchemaWithOptionsRecurse.object); +expectType(barSchemaWithOptionsRecurse.string); +expectType(barSchemaWithOptionsRecurse.number); +expectType(barSchemaWithOptionsRecurse.boolean); +expectType(barSchemaWithOptionsRecurse.symbol); +expectType(barSchemaWithOptionsRecurse.map); +expectType(barSchemaWithOptionsRecurse.set); +expectType(barSchemaWithOptionsRecurse.array); +expectType(barSchemaWithOptionsRecurse.tuple); +expectType>(barSchemaWithOptionsRecurse.objectArray); +expectType(barSchemaWithOptionsRecurse.readonlyMap); +expectType(barSchemaWithOptionsRecurse.readonlySet); +expectType(barSchemaWithOptionsRecurse.readonlyArray); +expectType(barSchemaWithOptionsRecurse.readonlyTuple); +expectType(barSchemaWithOptionsRecurse.regExp);