Description
openedon Nov 21, 2022
Suggestion
π Search Terms
inference, reverse mapped types, schema
β Viability Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
β Suggestion
It would be great if TypeScript could take into account concrete index types when inferring reverse mapped types.
Reverse mapped types are a great technique that allows us to create dependencies between properties in complex objects.
For example, in here we can validate what strings can be used as initial
property on any given level of this object. We can also "target" sibling keys (from the parent object) within the on
property.
This type of inference starts to break though once we add a constraint to T
in order to access some of its known properties upfront. Things like T[K]["type"]
prevents T
from being inferred because the implemented "pattern matching" isn't handling this case and without a special handling this introduces, sort of, a circularity problem. Note how this doesn't infer properly based on the given argument: here
I think there is a great potential here if we'd consider those accessed while inferring.
π Motivating Example
interface QueryFunctionContext<
TQueryKey extends string,
> {
queryKey: TQueryKey
}
type QueryOptions = {
key: string
data?: unknown;
fnData?: unknown;
}
type UseQueriesOptions<T extends ReadonlyArray<QueryOptions>> = {
[K in keyof T]: {
queryKey: T[K]['key']
queryFn?: (
ctx: QueryFunctionContext<T[K]['key']>,
) => Promise<T[K]['fnData']> | T[K]['fnData']
select?: (data: T[K]['fnData']) => T[K]['data']
}
}
declare function useQueries<
T extends ReadonlyArray<QueryOptions>
>(queries: [...UseQueriesOptions<T>]): void;
Old example
I understand this this particular example looks complex. I'm merely using it as a motivating example to showcase what I'm trying to do:
- limit what kind of values are possible for the
initial
property (based on the keys of the inferred object) - make this property available conditionally - it shouldn't be allowed where the
type
property of the "current" object is'paralel'
A way simpler demo of the inference algorithm shortcomings for this kind of things has been mentioned above (playground link)
type IsNever<T> = [T] extends [never] ? true : false;
type StateType = "parallel" | "final" | "compound" | "atomic";
type StateDefinition = {
type?: StateType;
states?: Record<string, StateDefinition>;
};
type State<T extends StateDefinition> = (T["type"] extends
| "parallel"
| undefined
? {}
: IsNever<keyof T["states"]> extends false
? { initial: keyof T["states"] }
: {}) & {
type?: T["type"];
states?: {
[K in keyof T["states"]]: State<T["states"][K] & {}> & {
on?: Record<string, keyof T["states"]>;
};
};
};
declare function createMachine<T extends StateDefinition>(config: State<T>): T;
createMachine({
// initial property should be available if there are any nested states and if the `type` of this object is not `'parallel'`
initial: "a",
states: {
a: {},
},
});
π» Use Cases
Schema-like APIs could leverage this a ton:
- we could use is at XState to type our state machines
- libraries related to JSON schema could use this
- I bet that TanStack libraries could think of neat ways to leverage this
- I'm pretty sure that Redux Toolkit could use this instead of "validating things through intersections"
Implementation
I'm willing to work on the implementation but I could use help with figuring out the exact constraints of the algorithm.
I've created locally a promising spike by gathering potential properties on the inference info when the objectType
has available index info in this inference round (here) and creating a type out of those when there is no other candidate for it here