Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema: Add recurseIntoArrays option #960

Merged
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
iamandrewluca marked this conversation as resolved.
Show resolved Hide resolved
? 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
iamandrewluca marked this conversation as resolved.
Show resolved Hide resolved
? 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
? K
: Schema<ObjectType[KeyType], K, Options>
: Schema<ObjectType[KeyType], K, Options> | K;
};

/**
@see Schema
*/
export type SchemaOptions = {
iamandrewluca marked this conversation as resolved.
Show resolved Hide resolved
/**
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;
};
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}>;

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}>;
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>;
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);