Skip to content

Improve reverse mapped types inference by creating candidates from concrete index typesΒ #51612

Open

Description

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    Experimentation NeededSomeone needs to try this out to see what happensSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions