So here's what I came up with for the "less of an esoteric example" (playground):
const ExampleComponent = (props: { hey: string }) => ({ hey: props.hey });
interface Person {
age: number;
name: string;
}
interface Transition {
params: <T>() => T;
}
const transition = { params: () => ({ age: 1, name: "aaa" }) };
const exampleStateDefinition = {
name: "example.state",
url: "/:name/:age",
component: ExampleComponent,
resolve: [
{
token: "age" as const,
deps: [transition],
resolveFn: (trans: Transition) => trans.params<Person>().age,
},
{
token: "name" as const,
deps: [transition],
resolveFn: (trans: Transition) => trans.params<Person>().name,
},
{
token: "sayHello" as const,
deps: [transition],
resolveFn: () => (greeting: string) => console.log(greeting),
},
],
};
type ResolverIn<T, R> = {
token: T;
resolveFn: R;
};
type ResolverOut<T, R> = {
token: T;
resolve: R;
};
type T1<T> = T extends Array<infer U>
? U extends ResolverIn<infer V, infer R>
? R extends (...args: any) => infer Ret
? ResolverOut<V, Ret>
: never
: never
: never;
type ResolveTypes = T1<typeof exampleStateDefinition["resolve"]>;
// = ResolverOut<"age", number> | ResolverOut<"name", string> | ResolverOut<"sayHello", (greeting: string) => void>
// which is:
// { token: "age", resolve: number } | { token: "name", resolve: string } | { token: "sayHello", resolve: (greeting: string) => void }
type StateDeclaration = {
resolve: ResolveTypes;
};
With the meat of the matter being
type T1<T> = T extends Array<infer U>
? U extends ResolverIn<infer V, infer R>
? R extends (...args: any) => infer Ret
? ResolverOut<V, Ret>
: never
: never
: never;
Here I first use a conditional to ensure the type is an array and infer the type of the array elements, then infer the shape to be ResolverIn
(sort of a Pick<>
), then infer the return type of the resolveFn
function (like ReturnType<T>
, but we've just inferred the type so we need to infer
again to further constrain the type to be a function) and finally produce the shape that we want which is ResolverOut<V, Ret>
.
The type of ResolveTypes
thus becomes:
ResolverOut<"age", number> |
ResolverOut<"name", string> |
ResolverOut<"sayHello", (greeting: string) => void>
whose shape is equivalent to:
{ token: "age"; resolve: number } |
{ token: "name"; resolve: string } |
{ token: "sayHello"; resolve: (greeting: string) => void }
Additionally, your example excludes the resolver types whose return value is a function, which can be filtered out with another conditional:
type T1<T> = T extends Array<infer U>
? U extends ResolverIn<infer V, infer R>
? R extends (...args: any) => infer Ret
? Ret extends (...args: any) => any
? never
: ResolverOut<V, Ret>
: never
: never
: never;
Edit:
Now, I didn't get the chance to test this, but to produce StateDeclaration
directly from typeof exampleStateDefinition
, you can probably do something like this:
type T2<T> = T extends { resolve: infer U } ? { resolve: T1<U> } : never;
type StateDeclaration = T2<typeof exampleStateDefinition>;
Edit 2: I was able to get a bit closer to what you clarified in the comments with this answer which uses an utility function (which just returns the array passed to it as-is) to enforce that the array passed to it contains all elements from the union type. Playground.
interface Person {
age: number;
name: string;
}
interface Transition {
params: <T>() => T;
}
type ResolveType<T> = {
[K in keyof T]: { token: K; resolveFn: (...args: any[]) => T[K] };
}[keyof T];
type ResolveTypes<T> = ResolveType<T>[]
function arrayOfAll<T>() {
return function <U extends T[]>(array: U & ([T] extends [U[number]] ? unknown : 'Invalid')) {
return array;
};
}
interface CustomStateDeclaration<T> {
name: string;
url: string;
component: any;
resolve: ResolveTypes<T>;
}
type ExampleComponentProps = {
age: number;
name: string;
sayHello: (greeting: string) => string;
};
const arrayOfAllPersonResolveTypes = arrayOfAll<ResolveType<ExampleComponentProps>>()
// passes
const valid = arrayOfAllPersonResolveTypes([
{
token: "age" as const,
resolveFn: (trans: Transition) => trans.params<Person>().age,
},
{
token: "name" as const,
resolveFn: (trans: Transition) => trans.params<Person>().name,
},
{
token: "sayHello" as const,
resolveFn: () => (greeting: string) => `Hello, ${greeting}`,
},
])
// error; missing the "sayHello" token
const missing1 = arrayOfAllPersonResolveTypes([
{
token: "age" as const,
resolveFn: (trans: Transition) => trans.params<Person>().age,
},
{
token: "name" as const,
resolveFn: (trans: Transition) => trans.params<Person>().name,
}
])
// error; "name" token's resolveFn returns a number instead of a string
const wrongType = arrayOfAllPersonResolveTypes([
{
token: "age" as const,
resolveFn: (trans: Transition) => trans.params<Person>().age,
},
{
token: "name" as const,
resolveFn: (trans: Transition) => 123,
},
{
token: "sayHello" as const,
resolveFn: () => (greeting: string) => `Hello, ${greeting}`,
},
])
Could try making a type that does what the utility function does, or create a state definition factory/constructor that all state declarations need to be created with (enforced by a symbol, perhaps), which uses that utility function.