Skip to content

Commit

Permalink
Schema: Add recurseIntoArrays option (#960)
Browse files Browse the repository at this point in the history
Co-authored-by: Grigoris Christainas <[email protected]>
  • Loading branch information
2 people authored and sindresorhus committed Nov 27, 2024
1 parent d7b692b commit fbbb8ba
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 8 deletions.
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
57 changes: 50 additions & 7 deletions source/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface User {
created: Date;
active: boolean;
passwordHash: string;
attributes: ['Foo', 'Bar']
}
type UserMask = Schema<User, 'mask' | 'hide' | 'show'>;
Expand All @@ -32,12 +33,13 @@ const userMaskSettings: UserMask = {
created: 'show',
active: 'show',
passwordHash: 'hide',
attributes: ['mask', 'show']
}
```
@category Object
*/
export type Schema<ObjectType, ValueType> = ObjectType extends string
export type Schema<ObjectType, ValueType, Options extends SchemaOptions = {}> = ObjectType extends string
? ValueType
: ObjectType extends Map<unknown, unknown>
? ValueType
Expand All @@ -48,7 +50,9 @@ export type Schema<ObjectType, ValueType> = ObjectType extends string
: ObjectType extends ReadonlySet<unknown>
? ValueType
: ObjectType extends Array<infer U>
? Array<Schema<U, ValueType>>
? Options['recurseIntoArrays'] extends false | undefined
? ValueType
: Array<Schema<U, ValueType>>
: ObjectType extends (...arguments_: unknown[]) => unknown
? ValueType
: ObjectType extends Date
Expand All @@ -58,14 +62,53 @@ export type Schema<ObjectType, ValueType> = ObjectType extends string
: ObjectType extends RegExp
? ValueType
: ObjectType extends object
? SchemaObject<ObjectType, ValueType>
? SchemaObject<ObjectType, ValueType, Options>
: ValueType;

/**
Same as `Schema`, but accepts only `object`s as inputs. Internal helper for `Schema`.
*/
type SchemaObject<ObjectType extends object, K> = {
[KeyType in keyof ObjectType]: ObjectType[KeyType] extends readonly unknown[] | unknown[]
? Schema<ObjectType[KeyType], K>
: Schema<ObjectType[KeyType], K> | 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<ObjectType[KeyType], K, Options>
: Schema<ObjectType[KeyType], K, Options> | 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<User, 'mask' | 'hide' | 'show', {recurseIntoArrays: false}>;
const userMaskSettings: UserMask = {
id: 'show',
name: {
firstname: 'show',
lastname: 'mask',
},
created: 'show',
active: 'show',
passwordHash: 'hide',
attributes: 'hide'
}
```
@default true
*/
readonly recurseIntoArrays?: boolean | undefined;
};
74 changes: 74 additions & 0 deletions test-d/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,77 @@ expectType<ComplexOption>(complexBarSchema.readonlySet);
expectType<readonly ComplexOption[]>(complexBarSchema.readonlyArray);
expectType<readonly [ComplexOption]>(complexBarSchema.readonlyTuple);
expectType<ComplexOption>(complexBarSchema.regExp);

// With Options and `recurseIntoArrays` set to `false`
type FooSchemaWithOptionsNoRecurse = Schema<typeof foo, FooOption, {recurseIntoArrays: false | undefined}>;

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<FooSchemaWithOptionsNoRecurse>(foo);
expectNotAssignable<FooSchemaWithOptionsNoRecurse>({key: 'value'});
expectNotAssignable<FooSchemaWithOptionsNoRecurse>(new Date());
expectType<FooOption>(fooSchemaWithOptionsNoRecurse.baz);

const barSchemaWithOptionsNoRecurse = fooSchemaWithOptionsNoRecurse.bar as Schema<typeof foo['bar'], FooOption, {recurseIntoArrays: false | undefined}>;
expectType<FooOption>(barSchemaWithOptionsNoRecurse.function);
expectType<FooOption | {key: FooOption}>(barSchemaWithOptionsNoRecurse.object);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.string);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.number);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.boolean);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.symbol);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.map);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.set);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.array);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.tuple);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.objectArray);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.readonlyMap);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.readonlySet);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.readonlyArray);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.readonlyTuple);
expectType<FooOption>(barSchemaWithOptionsNoRecurse.regExp);

// With Options and `recurseIntoArrays` set to `true`
type FooSchemaWithOptionsRecurse = Schema<typeof foo, FooOption, {recurseIntoArrays: true}>;

expectNotAssignable<FooSchemaWithOptionsRecurse>(foo);
expectNotAssignable<FooSchemaWithOptionsRecurse>({key: 'value'});
expectNotAssignable<FooSchemaWithOptionsRecurse>(new Date());
expectType<FooOption>(fooSchema.baz);

const barSchemaWithOptionsRecurse = fooSchema.bar as Schema<typeof foo['bar'], FooOption, {recurseIntoArrays: true}>;
expectType<FooOption>(barSchemaWithOptionsRecurse.function);
expectType<FooOption | {key: FooOption}>(barSchemaWithOptionsRecurse.object);
expectType<FooOption>(barSchemaWithOptionsRecurse.string);
expectType<FooOption>(barSchemaWithOptionsRecurse.number);
expectType<FooOption>(barSchemaWithOptionsRecurse.boolean);
expectType<FooOption>(barSchemaWithOptionsRecurse.symbol);
expectType<FooOption>(barSchemaWithOptionsRecurse.map);
expectType<FooOption>(barSchemaWithOptionsRecurse.set);
expectType<FooOption[]>(barSchemaWithOptionsRecurse.array);
expectType<FooOption[]>(barSchemaWithOptionsRecurse.tuple);
expectType<Array<{key: FooOption}>>(barSchemaWithOptionsRecurse.objectArray);
expectType<FooOption>(barSchemaWithOptionsRecurse.readonlyMap);
expectType<FooOption>(barSchemaWithOptionsRecurse.readonlySet);
expectType<readonly FooOption[]>(barSchemaWithOptionsRecurse.readonlyArray);
expectType<readonly [FooOption]>(barSchemaWithOptionsRecurse.readonlyTuple);
expectType<FooOption>(barSchemaWithOptionsRecurse.regExp);

0 comments on commit fbbb8ba

Please sign in to comment.