After some tweaking and experimentation, I managed to come up with a solution that appears to be working partially:
type ActionResult<A extends Action<any>['name']> = { [action in Action<any> as A]: action extends { name: A, type?: { transform: (...args: any) => infer R } } ? R : never; }[A];
export default function useTransformer<IN, E extends keyof IN, M extends Record<keyof M, Action<E>>>(state: IN, mappings: M) {
type N = keyof M;
return {
form: {} as {
[K in E | N]: K extends N ? ActionResult<M[K]['name']> : (K extends keyof IN ? IN[K] : never);
},
};
}
// The form is expected to have the following type:
const form: {
range: Date[];
dateFrom: string;
dateTo: string;
simpleMap: string;
a: string;
b: string;
c: number;
};
There are two minor issues: it doesn't exclude fields used in transformers, and the generic type E
seems unnecessary (as I intended to omit fields but encountered limitations with TypeScript narrowing).
To address the first issue (and consequently the second), you can create a type like ActionConsumes
to determine which fields are consumed, for example:
interface ActionConsumesFields {
dateRange: 'from' | 'to';
test: 'field';
}
type ActionConsumes<A extends Action<any>> = {
[name in keyof ActionConsumesFields]:
A extends { name: name } & { [key in ActionConsumesFields[name]]: any } ? A[ActionConsumesFields[name]] : never }[keyof ActionConsumesFields];
export default function useTransformer<IN, E extends keyof IN, M extends Record<keyof M, Action<E>>>(state: IN, mappings: M) {
type N = keyof M;
type Consumed = ActionConsumes<M[keyof M]>;
return {
form: {} as {
[K in Exclude<E, Consumed> | N]: K extends N ? ActionResult<M[K]['name']> : (K extends keyof IN ? IN[K] : never);
},
};
}
// Updated form type
// (note the absence of dateFrom, dateTo, and a fields)
const form: {
range: Date[];
simpleMap: string;
b: string;
c: number;
};
(added the ActionConsumes
concept)
I might find a way to trick TypeScript into automatically generating ActionConsumerFields
after further investigation.
I've successfully implemented the feature to "auto-detect fields":
const _ACS = Symbol();
type ActionConsumesFields = {
[action in Action<typeof _ACS> as action['name']]: keyof { [key in keyof action as action[key] extends typeof _ACS ? key : never]: 1; };
};
// Resulting ActionConsumesFields type:
type ActionConsumesFields = {
dateRange: "from" | "to";
test: "field";
};