Skip to content

Commit 79722ec

Browse files
committed
Implement defineTypeFactory, .build() and defaultFields
1 parent 6f3df8d commit 79722ec

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

.eslintrc.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ module.exports = {
1111
es2022: true,
1212
node: true,
1313
},
14+
rules: {
15+
'no-use-before-define': 'off',
16+
},
1417
overrides: [
1518
{
1619
files: ['*.ts', '*.tsx', '*.cts', '*.mts'],

src/index.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { expectType } from 'ts-expect';
2+
import { expect, it, describe } from 'vitest';
3+
import { defineBookFactory, type Book } from './index.js';
4+
5+
describe('defineTypeFactory', () => {
6+
it('basic', async () => {
7+
const BookFactory = defineBookFactory({
8+
defaultFields: {
9+
id: async () => Promise.resolve('Book-1'),
10+
title: async () => Promise.resolve('ゆゆ式'),
11+
author: async () =>
12+
Promise.resolve({
13+
id: 'Author-1',
14+
name: '三上小又',
15+
books: [],
16+
}),
17+
},
18+
});
19+
const book1 = await BookFactory.build();
20+
expect(book1).toMatchInlineSnapshot(`
21+
{
22+
"author": {
23+
"books": [],
24+
"id": "Author-1",
25+
"name": "三上小又",
26+
},
27+
"id": "Book-1",
28+
"title": "ゆゆ式",
29+
}
30+
`);
31+
expectType<{
32+
id: string;
33+
title: string;
34+
author: {
35+
id: string;
36+
name: string;
37+
books: Book[];
38+
};
39+
}>(book1);
40+
// @ts-expect-error
41+
// eslint-disable-next-line no-unused-expressions
42+
book1.unknownField;
43+
});
44+
});

src/index.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,75 @@
1-
export { run } from './runner.js';
2-
export { add } from './math.js';
1+
export type Book = {
2+
id: string;
3+
title: string;
4+
author: Author;
5+
};
6+
export type Author = {
7+
id: string;
8+
name: string;
9+
books: Book[];
10+
};
11+
12+
/** Convert `{ a: number, b: { c: number } }` into `{ a: number | undefined, b: { c: number | undefined } | undefined }`. */
13+
type DeepOptional<T> = {
14+
[K in keyof T]: T[K] extends Record<string, unknown> ? DeepOptional<T[K]> | undefined : T[K] | undefined;
15+
};
16+
type Entries<T> = {
17+
[K in keyof T]: [K, T[K]];
18+
}[keyof T][];
19+
type FieldResolver<Field> = () => Promise<Field>;
20+
type ResolvedFields<FieldsResolver> = {
21+
[FieldName in keyof FieldsResolver]: FieldsResolver[FieldName] extends FieldResolver<infer Field> ? Field : never;
22+
};
23+
24+
interface BookFactoryInterfaceWithoutTraits<TOptions extends BookFactoryDefineOptions> {
25+
build<T extends Partial<DeepOptional<Book>>>(inputFields?: T): Promise<ResolvedFields<TOptions['defaultFields']> & T>;
26+
}
27+
interface BookFactoryDefineOptions {
28+
defaultFields: {
29+
[Key in keyof Book]: FieldResolver<DeepOptional<Book>[Key]>;
30+
};
31+
}
32+
type BookFactoryInterface<TOptions extends BookFactoryDefineOptions> = BookFactoryInterfaceWithoutTraits<TOptions>;
33+
34+
async function resolveFields<
35+
TOptions extends BookFactoryDefineOptions,
36+
InputFields extends Partial<DeepOptional<Book>>,
37+
>(
38+
defaultFieldResolvers: TOptions['defaultFields'],
39+
inputFields: InputFields,
40+
): Promise<ResolvedFields<TOptions['defaultFields']> & InputFields> {
41+
const fields: ResolvedFields<TOptions['defaultFields']> & InputFields = {} as never;
42+
for (const [key, defaultFieldResolver] of Object.entries(defaultFieldResolvers) as Entries<
43+
TOptions['defaultFields']
44+
>) {
45+
// eslint-disable-next-line no-await-in-loop
46+
fields[key] = inputFields[key as keyof InputFields] ?? (await defaultFieldResolver());
47+
}
48+
return fields;
49+
}
50+
51+
function defineBookFactoryInternal<const TOptions extends BookFactoryDefineOptions>({
52+
defaultFields: defaultFieldResolvers,
53+
}: TOptions): BookFactoryInterface<TOptions> {
54+
return {
55+
async build(inputFields) {
56+
const fields = await resolveFields(
57+
defaultFieldResolvers,
58+
inputFields ?? ({} as Exclude<typeof inputFields, undefined>),
59+
);
60+
return fields;
61+
},
62+
};
63+
}
64+
65+
/**
66+
* Define factory for {@link Book} model.
67+
*
68+
* @param options
69+
* @returns factory {@link BookFactoryInterface}
70+
*/
71+
export function defineBookFactory<TOptions extends BookFactoryDefineOptions>(
72+
options: TOptions,
73+
): BookFactoryInterface<TOptions> {
74+
return defineBookFactoryInternal(options);
75+
}

0 commit comments

Comments
 (0)