Skip to content

Commit 1a7bb13

Browse files
committed
implement nonOptionalFields option
1 parent 60f4940 commit 1a7bb13

File tree

8 files changed

+69
-27
lines changed

8 files changed

+69
-27
lines changed

src/code-generator.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ function generateFieldNamesDefinitionCode(typeInfo: TypeInfo): string {
4040
return `const ${name}FieldNames = [${joinedFieldNames}] as const;\n`;
4141
}
4242

43-
function generateTypeFactoryCode(typeInfo: TypeInfo): string {
43+
function generateTypeFactoryCode(config: Config, typeInfo: TypeInfo): string {
4444
const { name } = typeInfo;
45+
function wrapRequired(str: string) {
46+
return config.nonOptionalFields ? `Required<${str}>` : str;
47+
}
4548
return `
4649
export type ${name}FactoryDefineOptions<
4750
TransientFields extends Record<string, unknown>,
@@ -57,7 +60,7 @@ export type ${name}FactoryInterface<
5760
5861
export function define${name}FactoryInternal<
5962
TransientFields extends Record<string, unknown>,
60-
_DefaultFieldsResolver extends DefaultFieldsResolver<Optional${name} & TransientFields>,
63+
_DefaultFieldsResolver extends ${wrapRequired(`DefaultFieldsResolver<Optional${name} & TransientFields>`)},
6164
_Traits extends Traits<Optional${name}, TransientFields>,
6265
>(
6366
options: ${name}FactoryDefineOptions<TransientFields, _DefaultFieldsResolver, _Traits>,
@@ -72,7 +75,7 @@ export function define${name}FactoryInternal<
7275
* @returns factory {@link ${name}FactoryInterface}
7376
*/
7477
export function define${name}Factory<
75-
_DefaultFieldsResolver extends DefaultFieldsResolver<Optional${name}>,
78+
_DefaultFieldsResolver extends ${wrapRequired(`DefaultFieldsResolver<Optional${name}>`)},
7679
_Traits extends Traits<Optional${name}, {}>,
7780
>(
7881
options: ${name}FactoryDefineOptions<{}, _DefaultFieldsResolver, _Traits>,
@@ -90,7 +93,7 @@ export function generateCode(config: Config, typeInfos: TypeInfo[]): string {
9093
code += '\n';
9194
code += generateFieldNamesDefinitionCode(typeInfo);
9295
code += '\n';
93-
code += generateTypeFactoryCode(typeInfo);
96+
code += generateTypeFactoryCode(config, typeInfo);
9497
code += '\n';
9598
}
9699
return code;

src/config.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ describe('validateConfig', () => {
2626
'`options.skipIsAbstractType` must be a boolean',
2727
);
2828
});
29+
it('nonOptionalFields', () => {
30+
expect(() => validateConfig({ typesFile: './types', nonOptionalFields: oneOf([true, false]) })).not.toThrow();
31+
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();
32+
expect(() => validateConfig({ typesFile: './types', nonOptionalFields: 1 })).toThrow(
33+
'`options.nonOptionalFields` must be a boolean',
34+
);
35+
});
2936
it('typesPrefix', () => {
3037
expect(() => validateConfig({ typesFile: './types', typesPrefix: 'Prefix' })).not.toThrow();
3138
expect(() => validateConfig({ typesFile: './types' })).not.toThrow();

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type RawConfig = {
44
typesFile: string;
55
skipTypename?: RawTypesConfig['skipTypename'];
66
skipIsAbstractType?: boolean | undefined;
7+
nonOptionalFields?: boolean | undefined;
78
namingConvention?: RawTypesConfig['namingConvention'];
89
typesPrefix?: RawTypesConfig['typesPrefix'];
910
typesSuffix?: RawTypesConfig['typesSuffix'];
@@ -14,6 +15,7 @@ export type Config = {
1415
typesFile: string;
1516
skipTypename: Exclude<RawTypesConfig['skipTypename'], undefined>;
1617
skipIsAbstractType: boolean;
18+
nonOptionalFields: boolean;
1719
typesPrefix: Exclude<RawTypesConfig['typesPrefix'], undefined>;
1820
typesSuffix: Exclude<RawTypesConfig['typesSuffix'], undefined>;
1921
convert: ConvertFn;
@@ -36,6 +38,9 @@ export function validateConfig(rawConfig: unknown): asserts rawConfig is RawConf
3638
if ('skipIsAbstractType' in rawConfig && typeof rawConfig['skipIsAbstractType'] !== 'boolean') {
3739
throw new Error('`options.skipIsAbstractType` must be a boolean');
3840
}
41+
if ('nonOptionalFields' in rawConfig && typeof rawConfig['nonOptionalFields'] !== 'boolean') {
42+
throw new Error('`options.nonOptionalFields` must be a boolean');
43+
}
3944
if ('typesPrefix' in rawConfig && typeof rawConfig['typesPrefix'] !== 'string') {
4045
throw new Error('`options.typesPrefix` must be a string');
4146
}
@@ -49,6 +54,7 @@ export function normalizeConfig(rawConfig: RawConfig): Config {
4954
typesFile: rawConfig.typesFile,
5055
skipTypename: rawConfig.skipTypename ?? false,
5156
skipIsAbstractType: rawConfig.skipIsAbstractType ?? true,
57+
nonOptionalFields: rawConfig.nonOptionalFields ?? false,
5258
typesPrefix: rawConfig.typesPrefix ?? '',
5359
typesSuffix: rawConfig.typesSuffix ?? '',
5460
convert: rawConfig.namingConvention

src/helper/factory.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
FieldResolver,
99
} from './field-resolver.js';
1010
import { getSequenceCounter, resetSequence } from './sequence.js';
11-
import { Merge } from './util.js';
11+
import { Merge, StrictlyPick } from './util.js';
1212

1313
export type Traits<OptionalType extends Record<string, unknown>, TransientFields extends Record<string, unknown>> = {
1414
[traitName: string]: {
@@ -30,20 +30,22 @@ export interface TypeFactoryInterface<
3030
OptionalType extends Record<string, unknown>,
3131
TransientFields extends Record<string, unknown>,
3232
// NOTE: The constraints of _DefaultFieldsResolver are loose so that `Merge<_DefaultFieldsResolver, _Traits[T]['defaultFields']>` is accepted.
33-
_DefaultFieldsResolver extends Record<keyof OptionalType, FieldResolver<OptionalType & TransientFields, unknown>>,
33+
_DefaultFieldsResolver extends Partial<
34+
Record<keyof OptionalType, FieldResolver<OptionalType & TransientFields, unknown>>
35+
>,
3436
_Traits extends Traits<OptionalType, TransientFields>,
3537
> {
36-
build(): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>>;
38+
build(): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>>;
3739
build<T extends InputFieldsResolver<OptionalType & TransientFields>>(
3840
inputFieldsResolver: T,
39-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>>;
41+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>>;
4042
buildList(
4143
count: number,
42-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>[]>;
44+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<{}>>, keyof OptionalType>[]>;
4345
buildList<T extends InputFieldsResolver<OptionalType & TransientFields>>(
4446
count: number,
4547
inputFieldsResolver: T,
46-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]>;
48+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]>;
4749
use<T extends keyof _Traits>(
4850
traitName: T,
4951
): TypeFactoryInterface<
@@ -72,7 +74,7 @@ export function defineTypeFactoryInternal<
7274
return {
7375
async build<T extends InputFieldsResolver<OptionalType & TransientFields>>(
7476
inputFieldsResolver?: T,
75-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>> {
77+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>> {
7678
const seq = getSeq();
7779
return resolveFields<OptionalType, TransientFields, _DefaultFieldsResolver, T>(
7880
typeFieldNames,
@@ -84,8 +86,11 @@ export function defineTypeFactoryInternal<
8486
async buildList<T extends InputFieldsResolver<OptionalType & TransientFields>>(
8587
count: number,
8688
inputFieldsResolver?: T,
87-
): Promise<Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]> {
88-
const array: Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[] = [];
89+
): Promise<StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>, keyof OptionalType>[]> {
90+
const array: StrictlyPick<
91+
Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<T>>,
92+
keyof OptionalType
93+
>[] = [];
8994
for (let i = 0; i < count; i++) {
9095
if (inputFieldsResolver) {
9196
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-explicit-any

src/helper/field-resolver.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { DeepReadonly, Merge } from './util.js';
1+
import { DeepReadonly, Merge, StrictlyPick } from './util.js';
22

33
export type FieldResolverOptions<OptionalTypeWithTransientFields> = {
44
seq: number;
55
get: <FieldName extends keyof OptionalTypeWithTransientFields>(
66
fieldName: FieldName,
7-
) => Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]>>;
7+
) => Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]> | undefined>;
88
};
99

1010
export class Dynamic<OptionalTypeWithTransientFields, Field> {
@@ -29,15 +29,14 @@ export type FieldResolver<OptionalTypeWithTransientFields, Field> =
2929
| Dynamic<OptionalTypeWithTransientFields, Field>;
3030
/** The type of `defaultFields` option of `defineFactory` function. */
3131
export type DefaultFieldsResolver<OptionalTypeWithTransientFields> = {
32-
[FieldName in keyof OptionalTypeWithTransientFields]: FieldResolver<
32+
[FieldName in keyof OptionalTypeWithTransientFields]?: FieldResolver<
3333
OptionalTypeWithTransientFields,
3434
DeepReadonly<OptionalTypeWithTransientFields[FieldName]>
3535
>;
3636
};
3737
/** The type of `inputFields` option of `build` method. */
38-
export type InputFieldsResolver<OptionalTypeWithTransientFields> = Partial<
39-
DefaultFieldsResolver<OptionalTypeWithTransientFields>
40-
>;
38+
export type InputFieldsResolver<OptionalTypeWithTransientFields> =
39+
DefaultFieldsResolver<OptionalTypeWithTransientFields>;
4140

4241
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4342
export type ResolvedField<T extends FieldResolver<unknown, unknown>> = T extends FieldResolver<infer _, infer R>
@@ -59,7 +58,7 @@ export async function resolveFields<
5958
defaultFieldsResolver: _DefaultFieldsResolver,
6059
inputFieldsResolver: _InputFieldsResolver,
6160
): Promise<
62-
Pick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<_InputFieldsResolver>>, keyof OptionalType>
61+
StrictlyPick<Merge<ResolvedFields<_DefaultFieldsResolver>, ResolvedFields<_InputFieldsResolver>>, keyof OptionalType>
6362
> {
6463
type OptionalTypeWithTransientFields = OptionalType & TransientFields;
6564

@@ -79,13 +78,17 @@ export async function resolveFields<
7978

8079
async function resolveFieldAndUpdateCache<FieldName extends keyof OptionalTypeWithTransientFields>(
8180
fieldName: FieldName,
82-
): Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]>> {
81+
): Promise<DeepReadonly<OptionalTypeWithTransientFields[FieldName]> | undefined> {
8382
if (fieldName in fields) return fields[fieldName];
8483

85-
const fieldResolver =
86-
fieldName in inputFieldsResolver
87-
? inputFieldsResolver[fieldName as keyof _InputFieldsResolver]
88-
: defaultFieldsResolver[fieldName as keyof _DefaultFieldsResolver];
84+
let fieldResolver: FieldResolver<OptionalType & TransientFields, unknown>;
85+
if (fieldName in inputFieldsResolver) {
86+
fieldResolver = inputFieldsResolver[fieldName as keyof _InputFieldsResolver];
87+
} else if (fieldName in defaultFieldsResolver) {
88+
fieldResolver = defaultFieldsResolver[fieldName as keyof _DefaultFieldsResolver];
89+
} else {
90+
return undefined;
91+
}
8992

9093
// eslint-disable-next-line require-atomic-updates
9194
fields[fieldName] = await resolveField(options, fieldResolver);
@@ -97,7 +100,7 @@ export async function resolveFields<
97100
get: resolveFieldAndUpdateCache,
98101
};
99102

100-
for (const fieldName of Object.keys(defaultFieldsResolver) as (keyof OptionalType)[]) {
103+
for (const fieldName of fieldNames) {
101104
// eslint-disable-next-line no-await-in-loop
102105
await resolveFieldAndUpdateCache(fieldName);
103106
}

src/helper/util.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expectTypeOf, it } from 'vitest';
2-
import { DeepReadonly, type DeepOptional, type Merge } from './util.js';
2+
import { DeepReadonly, type DeepOptional, type Merge, StrictlyPick } from './util.js';
33

44
it('DeepOptional', () => {
55
type Input = {
@@ -45,3 +45,13 @@ it('Merge', () => {
4545
d: string;
4646
}>();
4747
});
48+
49+
it('StrictlyPick', () => {
50+
expectTypeOf<StrictlyPick<{ a: number; b: number; c: number }, 'a' | 'b'>>().toEqualTypeOf<{
51+
a: number;
52+
b: number;
53+
}>();
54+
expectTypeOf<StrictlyPick<{ a: number; c: number }, 'a' | 'b'>>().toEqualTypeOf<{
55+
a: number;
56+
}>();
57+
});

src/helper/util.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ export type Merge<F, S> = {
2525
? F[K]
2626
: never;
2727
};
28+
29+
/**
30+
* @example `StrictlyPick<{ a: number, c: number }, 'a' | 'b'>` is `{ a: number }`.
31+
*/
32+
export type StrictlyPick<T, K> = {
33+
[P in K & keyof T]: T[P];
34+
};

src/test/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function fakeConfig(args: Partial<Config> = {}): Config {
1212
typesFile: './types',
1313
skipTypename: true,
1414
skipIsAbstractType: true,
15+
nonOptionalFields: false,
1516
typesPrefix: '',
1617
typesSuffix: '',
1718
convert: convertFactory({}),

0 commit comments

Comments
 (0)