Skip to content

Commit 069305c

Browse files
authored
Fix type for Infering for public configs (#984)
1 parent bb9c607 commit 069305c

File tree

2 files changed

+305
-12
lines changed

2 files changed

+305
-12
lines changed
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import { describe, expect, it } from 'tstyche';
2+
3+
import type {
4+
ConfigAsyncResolverDefinition,
5+
ConfigSyncResolverDefinition,
6+
ConfigEnvDefinition,
7+
PublicConfigsDefinitions,
8+
InferLoadedConfig,
9+
} from '../config.types.js';
10+
11+
describe('ConfigEnvDefinition', () => {
12+
it('should require env and default', () => {
13+
expect<ConfigEnvDefinition>().type.toBeAssignableWith({
14+
env: 'DATABASE_URL',
15+
default: 'postgresql://localhost:5432/db',
16+
} as const);
17+
18+
expect<ConfigEnvDefinition>().type.not.toBeAssignableWith({
19+
default: 'postgresql://localhost:5432/db',
20+
// error: env is required
21+
} as const);
22+
23+
expect<ConfigEnvDefinition>().type.not.toBeAssignableWith({
24+
env: 'DATABASE_URL',
25+
// error: default is required
26+
} as const);
27+
});
28+
29+
it('should allow evaluateOn only be serverStart', () => {
30+
expect<ConfigEnvDefinition<true>>().type.not.toBeAssignableWith({
31+
env: 'DATABASE_URL',
32+
default: 'postgresql://localhost:5432/db',
33+
// error: evaluateOn can only be serverStart
34+
evaluateOn: 'request',
35+
} as const);
36+
expect<ConfigEnvDefinition<true>>().type.toBeAssignableWith({
37+
env: 'DATABASE_URL',
38+
default: 'postgresql://localhost:5432/db',
39+
evaluateOn: 'serverStart',
40+
} as const);
41+
});
42+
43+
it('should allow optional isPublic and evaluateOn', () => {
44+
expect<ConfigEnvDefinition<true>>().type.toBeAssignableWith({
45+
env: 'DATABASE_URL',
46+
default: 'postgresql://localhost:5432/db',
47+
isPublic: true,
48+
} as const);
49+
50+
expect<ConfigEnvDefinition<true>>().type.toBeAssignableWith({
51+
env: 'DATABASE_URL',
52+
default: 'postgresql://localhost:5432/db',
53+
evaluateOn: 'serverStart',
54+
} as const);
55+
});
56+
});
57+
58+
describe('ConfigAsyncResolverDefinition', () => {
59+
it('should require resolver and evaluateOn', () => {
60+
expect<
61+
ConfigAsyncResolverDefinition<{ id: string }, string, 'request'>
62+
>().type.toBeAssignableWith({
63+
resolver: async (args: { id: string }) => args.id,
64+
evaluateOn: 'request',
65+
} as const);
66+
67+
expect<
68+
ConfigAsyncResolverDefinition<{ id: string }, string, 'request'>
69+
>().type.not.toBeAssignableWith({
70+
resolver: async (args: { id: string }) => args.id,
71+
// error: evaluateOn is required
72+
} as const);
73+
74+
expect<
75+
ConfigAsyncResolverDefinition<{ id: string }, string, 'request'>
76+
>().type.not.toBeAssignableWith({
77+
evaluateOn: 'request',
78+
// error: resolver is required
79+
} as const);
80+
});
81+
82+
it('should accept resolver that returns a promise only', () => {
83+
expect<
84+
ConfigAsyncResolverDefinition<undefined, string, 'request'>
85+
>().type.toBeAssignableWith({
86+
resolver: async () => 'result',
87+
evaluateOn: 'request',
88+
} as const);
89+
expect<
90+
ConfigAsyncResolverDefinition<undefined, string, 'request'>
91+
>().type.toBeAssignableWith({
92+
resolver: () => Promise.resolve('result'),
93+
evaluateOn: 'request',
94+
} as const);
95+
96+
expect<
97+
ConfigAsyncResolverDefinition<undefined, string, 'request'>
98+
>().type.not.toBeAssignableWith({
99+
resolver: () => 'result',
100+
evaluateOn: 'request',
101+
} as const);
102+
});
103+
it('should force evaluateOn to be request for resolvers with args', () => {
104+
expect<
105+
ConfigAsyncResolverDefinition<{ id: string }, string, 'request'>
106+
>().type.toBeAssignableWith({
107+
resolver: async (args: { id: string }) => args.id,
108+
evaluateOn: 'request',
109+
} as const);
110+
111+
// TODO: @assem.hafez - find a way to have this test without having npm run typecheck command failing
112+
/* expect<
113+
ConfigAsyncResolverDefinition<{ id: string }, string, 'serverStart'>
114+
>().type.toRaiseError(); */
115+
});
116+
it('should allow evaluateOn to be serverStart or request for resolvers without args', () => {
117+
expect<
118+
ConfigAsyncResolverDefinition<undefined, string, 'request'>
119+
>().type.toBeAssignableWith({
120+
resolver: async () => 'result',
121+
evaluateOn: 'request',
122+
} as const);
123+
124+
expect<
125+
ConfigAsyncResolverDefinition<undefined, string, 'serverStart'>
126+
>().type.toBeAssignableWith({
127+
resolver: async () => 'result',
128+
evaluateOn: 'serverStart',
129+
} as const);
130+
});
131+
132+
it('should allow optional isPublic', () => {
133+
expect<
134+
ConfigAsyncResolverDefinition<{ id: string }, string, 'request', true>
135+
>().type.toBeAssignableWith({
136+
resolver: async (args: { id: string }) => args.id,
137+
evaluateOn: 'request',
138+
isPublic: true,
139+
} as const);
140+
141+
expect<
142+
ConfigAsyncResolverDefinition<{ id: string }, string, 'request', false>
143+
>().type.toBeAssignableWith({
144+
resolver: async (args: { id: string }) => args.id,
145+
evaluateOn: 'request',
146+
} as const);
147+
});
148+
});
149+
150+
describe('ConfigSyncResolverDefinition', () => {
151+
it('should require resolver and evaluateOn', () => {
152+
expect<
153+
ConfigSyncResolverDefinition<{ id: string }, string, 'request'>
154+
>().type.toBeAssignableWith({
155+
resolver: (args: { id: string }) => args.id,
156+
evaluateOn: 'request',
157+
} as const);
158+
159+
expect<
160+
ConfigSyncResolverDefinition<{ id: string }, string, 'request'>
161+
>().type.not.toBeAssignableWith({
162+
resolver: (args: { id: string }) => args.id,
163+
// error: evaluateOn is required
164+
} as const);
165+
166+
expect<
167+
ConfigSyncResolverDefinition<{ id: string }, string, 'request'>
168+
>().type.not.toBeAssignableWith({
169+
evaluateOn: 'request',
170+
// error: resolver is required
171+
} as const);
172+
});
173+
it('should accept resolver that returns a non promise', () => {
174+
expect<
175+
ConfigSyncResolverDefinition<undefined, string, 'request'>
176+
>().type.toBeAssignableWith({
177+
resolver: () => 'result',
178+
evaluateOn: 'request',
179+
} as const);
180+
expect<
181+
ConfigSyncResolverDefinition<undefined, string, 'request'>
182+
>().type.not.toBeAssignableWith({
183+
resolver: () => Promise.resolve('result'),
184+
evaluateOn: 'request',
185+
} as const);
186+
187+
expect<
188+
ConfigSyncResolverDefinition<undefined, string, 'request'>
189+
>().type.not.toBeAssignableWith({
190+
resolver: async () => 'result',
191+
evaluateOn: 'request',
192+
} as const);
193+
});
194+
195+
it('should force evaluateOn to be request for resolvers with args', () => {
196+
expect<
197+
ConfigSyncResolverDefinition<{ id: string }, string, 'request'>
198+
>().type.toBeAssignableWith({
199+
resolver: (args: { id: string }) => args.id,
200+
evaluateOn: 'request',
201+
} as const);
202+
203+
// TODO: @assem.hafez - find a way to have this test without having npm run typecheck command failing
204+
/* expect<
205+
ConfigAsyncResolverDefinition<{ id: string }, string, 'serverStart'>
206+
>().type.toRaiseError(); */
207+
});
208+
it('should allow evaluateOn to be serverStart or request for resolvers without args', () => {
209+
expect<
210+
ConfigSyncResolverDefinition<undefined, string, 'request'>
211+
>().type.toBeAssignableWith({
212+
resolver: () => 'result',
213+
evaluateOn: 'request',
214+
} as const);
215+
216+
expect<
217+
ConfigSyncResolverDefinition<undefined, string, 'serverStart'>
218+
>().type.toBeAssignableWith({
219+
resolver: () => 'result',
220+
evaluateOn: 'serverStart',
221+
} as const);
222+
});
223+
224+
it('should allow optional isPublic', () => {
225+
expect<
226+
ConfigSyncResolverDefinition<{ id: string }, string, 'request', true>
227+
>().type.toBeAssignableWith({
228+
resolver: (args: { id: string }) => args.id,
229+
evaluateOn: 'request',
230+
isPublic: true,
231+
} as const);
232+
233+
expect<
234+
ConfigSyncResolverDefinition<{ id: string }, string, 'request', false>
235+
>().type.toBeAssignableWith({
236+
resolver: (args: { id: string }) => args.id,
237+
evaluateOn: 'request',
238+
} as const);
239+
});
240+
});
241+
242+
describe('InferLoadedConfig', () => {
243+
it('should have configs with evaluateOn request as functions', () => {
244+
type LoadedConfig = InferLoadedConfig<{
245+
API_URL: ConfigSyncResolverDefinition<undefined, string, 'request'>;
246+
API_PORT: ConfigAsyncResolverDefinition<undefined, number, 'request'>;
247+
}>;
248+
expect<LoadedConfig>().type.toBe<{
249+
API_URL: () => string;
250+
API_PORT: () => Promise<number>;
251+
}>();
252+
});
253+
254+
it('should have configs with evaluateOn serverStart as the return type of the function', () => {
255+
type LoadedConfig = InferLoadedConfig<{
256+
DATABASE_URL: ConfigEnvDefinition;
257+
API_URL: ConfigSyncResolverDefinition<undefined, string, 'serverStart'>;
258+
API_PORT: ConfigAsyncResolverDefinition<undefined, number, 'serverStart'>;
259+
}>;
260+
expect<LoadedConfig>().type.toBe<{
261+
DATABASE_URL: string;
262+
API_URL: string;
263+
API_PORT: number;
264+
}>();
265+
});
266+
});
267+
268+
describe('PublicConfigsDefinitions', () => {
269+
it('should have the type of an enum with the keys of the config definitions that have isPublic true', () => {
270+
type PublicConfigs = PublicConfigsDefinitions<{
271+
DATABASE_URL: ConfigEnvDefinition;
272+
API_URL: ConfigSyncResolverDefinition<undefined, string, 'request'>;
273+
API_PORT: ConfigAsyncResolverDefinition<undefined, number, 'serverStart'>;
274+
DATABASE_URL_PUBLIC: ConfigEnvDefinition<true>;
275+
API_URL_PUBLIC: ConfigSyncResolverDefinition<
276+
undefined,
277+
string,
278+
'request',
279+
true
280+
>;
281+
API_PORT_PUBLIC: ConfigAsyncResolverDefinition<
282+
undefined,
283+
number,
284+
'serverStart',
285+
true
286+
>;
287+
}>;
288+
expect<keyof PublicConfigs>().type.toBe<
289+
'API_URL_PUBLIC' | 'API_PORT_PUBLIC' | 'DATABASE_URL_PUBLIC'
290+
>();
291+
});
292+
});

src/utils/config/config.types.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,20 @@ export type InferLoadedConfig<T extends Record<string, ConfigDefinition>> = {
6363
: T[K] extends ResolverType<any, infer ReturnType, infer EvalOn, any>
6464
? EvalOn extends 'serverStart'
6565
? ReturnType // If it's a sync resolver with evaluateOn serverStart, return the type directly
66-
: T[K] extends ConfigSyncResolverDefinition<
66+
: T[K] extends ConfigAsyncResolverDefinition<
6767
infer Args,
6868
infer ReturnType,
6969
any,
7070
any
7171
>
72-
? ConfigResolver<Args, ReturnType> // If it's a sync resolver, it's a function with matching signature
73-
: T[K] extends ConfigAsyncResolverDefinition<
72+
? ConfigResolver<Args, Promise<ReturnType>> // If it's an async resolver, it's a promise-returning function
73+
: T[K] extends ConfigSyncResolverDefinition<
7474
infer Args,
7575
infer ReturnType,
7676
any,
7777
any
7878
>
79-
? ConfigResolver<Args, Promise<ReturnType>> // If it's an async resolver, it's a promise-returning function
79+
? ConfigResolver<Args, ReturnType> // If it's a sync resolver, it's a function with matching signature
8080
: never // If it doesn't match any known type, it's never
8181
: never; //If it doesn't match any known type, it's never
8282
};
@@ -92,14 +92,15 @@ export type ServerStartEvaluatedConfigDefintions<
9292
: never]: T[K];
9393
};
9494

95-
type PublicConfigsDefinitions<T extends ConfigDefinitionRecords> = OmitNever<{
96-
[K in keyof T]: T[K] extends
97-
| ConfigEnvDefinition<true>
98-
| ConfigAsyncResolverDefinition<any, any, any, true>
99-
| ConfigSyncResolverDefinition<any, any, any, true>
100-
? T[K]
101-
: never;
102-
}>;
95+
export type PublicConfigsDefinitions<T extends ConfigDefinitionRecords> =
96+
OmitNever<{
97+
[K in keyof T]: T[K] extends
98+
| ConfigEnvDefinition<true>
99+
| ConfigAsyncResolverDefinition<any, any, any, true>
100+
| ConfigSyncResolverDefinition<any, any, any, true>
101+
? T[K]
102+
: never;
103+
}>;
103104

104105
type ConfigResolvedValues<T extends ConfigDefinitionRecords> = {
105106
[K in keyof T]: T[K] extends ResolverType<any, infer ReturnType, any, any>

0 commit comments

Comments
 (0)