Open dubzzz opened 8 months ago
First drafts:
type Pretty<T> = { [K in keyof T]: T[K] } & {}
type IsOptional<TType, TKey extends keyof TType> = Pick<TType, TKey> extends Record<TKey, TType[TKey]> ? false : true;
const is1: Pretty<IsOptional<{s?: string}, 's'>> = true;
const is2: Pretty<IsOptional<{s?: string|undefined}, 's'>> = true;
const isNot1: Pretty<IsOptional<{s: string|undefined}, 's'>> = false;
const isNot2: Pretty<IsOptional<{s: string}, 's'>> = false;
const isNot3: Pretty<IsOptional<{s: undefined}, 's'>> = false;
type OptionalKeysOf<TType> = keyof { [K in keyof TType as IsOptional<TType, K> extends true ? K : never]: never }
type RequiredKeysOf<TType> = keyof { [K in keyof TType as IsOptional<TType, K> extends true ? never : K]: never }
type PPP = RequiredKeysOf<{s?: string, n: number | undefined }>
type RecordConstraints<TOut> = {requiredKeys: readonly RequiredKeysOf<TOut>[]}
// "All keys required" matches "Some keys potentially optional"
declare function record<TOut>(
shape:
{ [K in keyof TOut as IsOptional<TOut, K> extends true ? never : K]: Arbitrary<TOut[K]> }
& { [K in keyof TOut as IsOptional<TOut, K> extends true ? K : never]?: Arbitrary<TOut[K]> | Arbitrary<Exclude<TOut[K], undefined>> },
): TOut;
declare function record<TOut, TConstraints extends RecordConstraints<TOut>>(
shape:
{ [K in keyof TOut as IsOptional<TOut, K> extends true ? never : K]: Arbitrary<TOut[K]> }
& { [K in keyof TOut as IsOptional<TOut, K> extends true ? K : never]?: Arbitrary<TOut[K]> | Arbitrary<Exclude<TOut[K], undefined>> },
options: TConstraints
): Pretty<TConstraints>;
const a1 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() }
)
const a2 = record<{s?: string, n?: number }>(
{ s: arbString(), n: arbNat() },
)
const a22 = record<{s?: string, n?: number }>( // we have to make sure arbNatOrUndefined would not pass as n? means number when set, not number|undefined
{ s: arbString(), n: arbNatOrUndefined() },
)
const a3 = record<{s?: string, n?: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: [] }
)
const a4 = record<{s?: string, n?: number }>(
{ }
)
const a5 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: ['s', 'n'] as const }
)
// Should not compile
const b1 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: [] }
)
const b2 = record<{s?: string, n: number | undefined }>(
{ }
)
A step forward:
type Arbitrary<T> = {
generate: () => T;
shrink: (value: T) => IterableIterator<T>;
}
declare function arbNat(): Arbitrary<number>;
declare function arbNatOrUndefined(): Arbitrary<number|undefined>;
declare function arbString(): Arbitrary<string>;
declare function arbConstant<T>(v: T): Arbitrary<T>;
type Pretty<T> = { [K in keyof T]: T[K] } & {}
type IsOptional<TType, TKey extends keyof TType> = Pick<TType, TKey> extends Record<TKey, TType[TKey]> ? false : true;
const is1: Pretty<IsOptional<{s?: string}, 's'>> = true;
const is2: Pretty<IsOptional<{s?: string|undefined}, 's'>> = true;
const isNot1: Pretty<IsOptional<{s: string|undefined}, 's'>> = false;
const isNot2: Pretty<IsOptional<{s: string}, 's'>> = false;
const isNot3: Pretty<IsOptional<{s: undefined}, 's'>> = false;
type OptionalKeysOf<TType> = keyof { [K in keyof TType as IsOptional<TType, K> extends true ? K : never]: never }
type RequiredKeysOf<TType> = keyof { [K in keyof TType as IsOptional<TType, K> extends true ? never : K]: never }
type PPP = RequiredKeysOf<{s?: string, n: number | undefined }>
// From https://github.com/Microsoft/TypeScript/issues/13298
type UnionToIntersection<U> = (U extends never ? never : (arg: U) => never) extends (arg: infer I) => void ? I : never;
type UnionToTuple<T, A extends any[] = []> = UnionToIntersection<T extends never ? never : (t: T) => T> extends (_: never) => infer W ? UnionToTuple<Exclude<T, W>, [...A, W]> : A;
type QQQ = UnionToTuple<"a" | "b" | "c">
type RecordShape<TOut> =
{ [K in keyof TOut as IsOptional<TOut, K> extends true ? never : K]: Arbitrary<TOut[K]> }
& { [K in keyof TOut as IsOptional<TOut, K> extends true ? K : never]?: Arbitrary<TOut[K]> | Arbitrary<Exclude<TOut[K], undefined>> };
type RecordConstraints<TOut> = {requiredKeys: UnionToTuple<RequiredKeysOf<TOut>>}
declare function record<TOut>(shape: RecordShape<TOut>): TOut;
declare function record<TOut>(
shape: RecordShape<TOut>,
options: RecordConstraints<TOut>
): TOut;
const a1 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() }
)
const a2 = record<{s?: string, n?: number }>(
{ s: arbString(), n: arbNat() },
)
const a22 = record<{s?: string, n?: number }>( // we have to make sure arbNatOrUndefined would not pass as n? means number when set, not number|undefined
{ s: arbString(), n: arbNatOrUndefined() },
)
const a3 = record<{s?: string, n?: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: [] }
)
const a4 = record<{s?: string, n?: number }>(
{ }
)
const a5 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: ['n', 's'] }
)
const a6 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: ['s', 'n'] }
)
// Should not compile
const b1 = record<{s: string, n: number }>(
{ s: arbString(), n: arbNat() },
{ requiredKeys: [] }
)
const b2 = record<{s?: string, n: number | undefined }>(
{ }
)
Here is a Playground to play with to adapt current typings for record: link.
This looks good! Feel free to DM me on Discord/Twitter and we can schedule a chat next week and go through the Playground link.
I might have some ideas for handling tuple inputs that could be useful as well as testing + type errors and would be happy to discuss if it would be helpful to an awesome library like this!
I played again on this one, but so far I have not succeeded into finding something that could work when the users explicitly specify something between the <?>
and when they don't. The following version seems to work quite well (just for a few keys being required) with <?>
but sucks for the other:
// Playground to build record
// record in v3+ (v3 with options dropped to be closer to v4)
type RecordConstraints<T extends string | number | symbol> = { requiredKeys?: ArrayWithAllKeys<T>; noNullPrototype?: boolean; };
type NonOptionalKeyOf<T> = keyof { [K in keyof T as undefined extends T[K] ? never : K]: K }
type Flatten<T> = T extends [infer THead, ...infer TRest]
? THead extends [infer TNestedHead, ...infer TNestedRest]
? Flatten<[TNestedHead, ...TNestedRest, ...TRest]>
: THead extends []
? Flatten<[...TRest]>
: [THead, ...Flatten<TRest>]
: T;
type ArrayWithAllKeys<TKeys extends string | number | symbol> = [TKeys] extends [never] ? [] : { [K in TKeys]: Flatten<[K, ArrayWithAllKeys<Exclude<TKeys, K>>]> }[TKeys]
type PP = ArrayWithAllKeys<"a" | "b">
declare function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }): Arbitrary<T>;
declare function record<T>(recordModel: { [K in keyof T]: Arbitrary<T[K]> }, constraints: RecordConstraints<NonOptionalKeyOf<T>>): Arbitrary<T>;
// Fake definitions for fast-check
class Arbitrary<T> {
generate(): T { throw new Error("Not implemented!"); }
shrink(value: T): IterableIterator<T> { throw new Error("Not implemented!"); }
}
declare function arbNat(): Arbitrary<number>;
declare function arbOption<T, TNil = null>(arb: Arbitrary<T>, constraints?: {nil?: TNil}): Arbitrary<T | TNil>;
declare function arbString(): Arbitrary<string>;
declare function arbConstant<T>(v: T): Arbitrary<T>;
const fc = { Arbitrary, nat: arbNat, constant: arbConstant, string: arbString, option: arbOption, record };
// New set of tests that we want to make pass for v4
expectType<Arbitrary<{ s: string }>>()(
record<{s: string}>({ s: fc.string() }),
""
);
expectType<Arbitrary<{ s: string }>>()(
record<{s: string}>({ s: fc.string() }, {}),
""
);
expectType<Arbitrary<{ s: string }>>()(
record<{s: string}>({ s: fc.string() }, { requiredKeys: ['s'] }),
""
);
expectType<Arbitrary<{ s: string }>>()(
record({ s: fc.string() }, {}),
""
);
expectType<Arbitrary<{ s: string }>>()(
record({ s: fc.string() }, { requiredKeys: ['s'] }),
""
);
expectType<Arbitrary<{ s?: string }>>()(
record({ s: fc.string() }, { requiredKeys: [] }),
""
);
expectType<Arbitrary<{ s?: string }>>()(
record<{s?: string}>({ s: fc.string() }, { requiredKeys: [] }),
""
);
expectType<Arbitrary<{ s?: string | undefined }>>()(
record<{s?: string}>({ s: fc.option(fc.string(), {nil:undefined}) }, { requiredKeys: [] }),
""
);
expectType<Arbitrary<{ a: string, b?: number, c: number | undefined }>>()(
record<{ a: string, b?: number, c: number | undefined }>({ a: fc.string(), b: fc.nat(), c: fc.option(fc.nat(), {nil:undefined}) }, { requiredKeys: ['a', 'c'] }),
""
);
expectType<Arbitrary<{ a: string, b?: number, c: number | undefined }>>()(
record<{ a: string, b?: number, c: number | undefined }>({ a: fc.string(), b: fc.nat(), c: fc.option(fc.nat(), {nil:undefined}) }, { requiredKeys: ['c', 'a'] }),
"same as above but with b and c reversed"
);
// @ts-expect-error - should not pass when keys are missing (missing b)
record<{ a: string, b?: number, c: number | undefined }>({ a: fc.string(), b: fc.nat(), c: fc.option(fc.nat(), {nil:undefined}) }, { requiredKeys: ['c'] });
// @ts-expect-error - should not pass when keys linked to otpional keys are added
record<{ a: string, b?: number, c: number | undefined }>({ a: fc.string(), b: fc.nat(), c: fc.option(fc.nat(), {nil:undefined}) }, { requiredKeys: ['a', 'b', 'c'] });
// @ts-expect-error - should not pass when optional and no requiredKeys specified
record<{ a: string, b?: number, c: number | undefined }>({ a: fc.string(), b: fc.nat(), c: fc.option(fc.nat(), {nil:undefined}) });
// Tests running on the current version
declare const mySymbol1: unique symbol;
declare const mySymbol2: unique symbol;
expectType<Arbitrary<{ a: number; b: string }>>()(
fc.record({ a: fc.nat(), b: fc.string() }),
'"record" can contain multiple types',
);
expectType<Arbitrary<{ [mySymbol1]: number; [mySymbol2]: string }>>()(
fc.record({ [mySymbol1]: fc.nat(), [mySymbol2]: fc.string() }),
'"record" can be indexed using unique symbols as keys',
);
expectType<Arbitrary<{ a: number; b: string }>>()(
fc.record({ a: fc.nat(), b: fc.string() }, {}),
'"record" accepts empty constraints',
);
expectType<Arbitrary<{ a?: number; b?: string }>>()(
fc.record({ a: fc.nat(), b: fc.string() }, { requiredKeys: [] }),
'"record" only applies optional on keys declared within requiredKeys even when empty',
);
expectType<Arbitrary<{ a: number; b?: string }>>()(
fc.record({ a: fc.nat(), b: fc.string() }, { requiredKeys: ['a'] }),
'"record" only applies optional on keys declared within requiredKeys even if unique',
);
expectType<Arbitrary<{ a: number; b?: string; c: string }>>()(
fc.record({ a: fc.nat(), b: fc.string(), c: fc.string() }, { requiredKeys: ['a', 'c'] }),
'"record" only applies optional on keys declared within requiredKeys even if multiple ones specified',
);
expectType<Arbitrary<{ a?: number; b?: string | undefined }>>()(
fc.record({ a: fc.nat(), b: fc.option(fc.string(), { nil: undefined }) }, { requiredKeys: [] }),
'"record" only applies optional on keys declared within requiredKeys and preserves existing |undefined when adding ?',
);
expectType<Arbitrary<{ [mySymbol1]: number; [mySymbol2]?: string }>>()(
fc.record({ [mySymbol1]: fc.nat(), [mySymbol2]: fc.string() }, { requiredKeys: [mySymbol1] as [typeof mySymbol1] }),
'"record" only applies optional on keys declared within requiredKeys even if it contains symbols',
);
expectType<Arbitrary<{ [mySymbol1]: number; [mySymbol2]?: string; a: number; b?: string }>>()(
fc.record(
{ [mySymbol1]: fc.nat(), [mySymbol2]: fc.string(), a: fc.nat(), b: fc.string() },
{ requiredKeys: [mySymbol1, 'a'] as [typeof mySymbol1, 'a'] },
),
'"record" only applies optional on keys declared within requiredKeys even if it contains symbols and normal keys',
);
type Query = { data: { field: 'X' } };
expectType<Arbitrary<Query>>()(
// issue 1453
fc.record<Query>({ data: fc.record({ field: fc.constant('X') }) }),
'"record" can be passed the requested type in <*>',
);
expectType<Arbitrary<Partial<Query>>>()(
// issue 1453
fc.record<Partial<Query>>({ data: fc.record({ field: fc.constant('X') }) }),
'"record" can be passed something assignable to the requested type in <*>',
);
// @ts-expect-error - requiredKeys references an unknown key
fc.record({ a: fc.nat(), b: fc.string() }, { requiredKeys: ['c'] });
// @ts-expect-error - record expects arbitraries not raw values
fc.record({ a: 1 });
// MUST ERROR with exactOptionalPropertyTypes
expectType<Arbitrary<{ a?: number; b?: string | undefined }>>()(fc.record({ a: fc.nat(), b: fc.string() }, { requiredKeys: [] }), '"record" only applies optional on keys declared within requiredKeys by adding ? without |undefined');
// Assertions on types copied from @fast-check/expect-type
declare type Not<T> = T extends true ? false : true;
declare type And<T, U> = T extends true ? (U extends true ? true : false) : false;
declare type Or<T, U> = T extends false ? (U extends false ? false : true) : true;
declare type IsNever<T> = [T] extends [never] ? true : false;
declare type Extends<T, U> = T extends U ? true : false;
declare type ExtendsString<T> = Extends<T, string> extends boolean ? boolean extends Extends<T, string> ? true : false : false; // Extends<T, string> is: false for unknown but boolean for any
declare type IsUnknown<T> = And<And<Not<IsNever<T>>, Extends<T, unknown>>, And<Extends<unknown, T>, Not<ExtendsString<T>>>>;
declare type IsAny<T> = And<And<Not<IsNever<T>>, Not<IsUnknown<T>>>, And<Extends<T, any>, Extends<any, T> extends true ? true : false>>;
declare type DeeperIsSame<T, U> = IsAny<T> extends false ? T extends object ? { [K in keyof (T | U)]: IsSame<T[K], U[K]> } extends { [K in keyof (T | U)]: true } ? true : false : true : false;
declare type IsSame<T, U> = [T, U] extends [U, T] ? Or<Or<Or<And<IsAny<T>, IsAny<U>>, And<IsUnknown<T>, IsUnknown<U>>>, And<IsNever<T>, IsNever<U>>>, And<And<And<And<Not<IsAny<T>>, Not<IsAny<U>>>, And<Not<IsUnknown<T>>, Not<IsUnknown<U>>>>,And<Not<IsNever<T>>, Not<IsNever<U>>>>, DeeperIsSame<T, U>>> : false;
declare function expectType<TExpectedType>(): <TReal>(arg: TReal,...noArgs: IsSame<TExpectedType, TReal> extends true ? [string] : [{ expected: TExpectedType; got: TReal }]) => void;
declare function expectTypeAssignable<TExpectedType>(): <TReal>(arg: TReal,...noArgs: Extends<TReal, TExpectedType> extends true ? [string] : [{ expected: TExpectedType; got: TReal }]) => void;
I tried playing with NoInfer
but I failed. I believe the thing building the tuples of all required keys sucks.
💡 Idea
The aim would be to simplify the typings (from a user point-of-view) of record. We want he final users to be able to write
record<ExpectedTypeWithNullable>
.See playground below for the requirements: https://github.com/dubzzz/fast-check/issues/4570#issuecomment-1878264000