TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Improve reverse mapped types inference by creating candidates from concrete index types

Open Andarist opened this issue 3 years ago β€’ 7 comments

Suggestion

πŸ” Search Terms

inference, reverse mapped types, schema

βœ… Viability Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • [x] 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

Andarist avatar Nov 21 '22 13:11 Andarist

I understand this this particular example looks complex

A less complex example for novices such as myself would be appreciated, lol 😰. Something that would make sense in a blog post, for example, tends to be pretty compelling.

RyanCavanaugh avatar Dec 01 '22 21:12 RyanCavanaugh

How about this one:

type StateType = "parallel" | "final" | "compound" | "atomic";
type StateSchema = Record<string, { type: StateType }>;

declare function createMachine<T extends StateSchema>(
  obj: {
    [K in keyof T]: {
      type: T[K]["type"];
    } & (T[K]["type"] extends "final"
      ? {}
      : {
          on: Record<string, keyof T>;
        });
  }
): T;

In here, I restrict the presence of on property within the "current" state - it shouldn't be available on a state of type 'final'. I'd like for this reverse mapped type to be inferred as the same type that I provide explicitly here

Andarist avatar Dec 01 '22 23:12 Andarist

@RyanCavanaugh I believe that this proposal has a lot of potential to simplify types of some popular libraries, like React Query, Redux Toolkit, and more.

The most recent example of problems that people struggle with can be found in this thread. At the moment, they resort to recursive conditional types but this technique fails to infer unannotated arguments within tuple elements - this is something that works just great with reverse mapped types. The problem is though that they need to infer multiple different things per tuple element and that isn't possible right now - but it could be, with this proposal implemented.

Andarist avatar Dec 31 '22 11:12 Andarist

@RyanCavanaugh maybe I can provide a "real life" example of where this can be useful - from Redux Toolkit.

At the moment, it kinda works, but our types to enforce this are pretty wonky; there is not much inference, and we already had the case where a TS PR had to be rolled back until we could figure out a fix. Better support from TS would be highly appreciated! Playground Link

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

interface State {
  foo: string;
}

const slice = createSlice({
  name: "someSlice",
  initialState: {
    foo: "bar",
  } satisfies State as State,
  reducers: {
    // simple notation - this is easy for us
    simpleReducer(state, action: PayloadAction<string>) {
      state.foo = action.payload;
    },
    // notation that is "split" into a `prepare` function that creates the `action` that will be passed into `reducer`
    reducerWithPrepareNotation: {
      reducer(state, action: PayloadAction<string>) {
        state.foo = action.payload;
      },
      prepare(char: string, repeats: number) {
        return { payload: char.repeat(repeats) };
      },
    },
    // another version with a different action type - but still matching between `reducer` and `prepare`
    reducerWithAnotherPrepareNotation: {
      reducer(state, action: PayloadAction<number>) {
        state.foo = state.foo.slice(0, action.payload);
      },
      prepare(char: string, repeats: number) {
        return { payload: repeats * char.length };
      },
    },
    /* uncomment to see the error. This is a "wrong user code" that we want to protect against.
    invalidReducerWithPrepareNotation: {
      reducer(state, action: PayloadAction<string>) {
        state.foo = action.payload
      },
      // @ts-expect-error we want this to error, because it returns { payload: number }, while the `reducer` above requires { payload: string } as second argument
      prepare(char: string, repeats: number) {
        return { payload: repeats * char.length }
      }
    },
    */
  },
});

{
  const _expectType: (payload: string) => PayloadAction<string> =
    slice.actions.simpleReducer;
}
{
  const _expectType: (char: string, repeats: number) => PayloadAction<string> =
    slice.actions.reducerWithPrepareNotation;
}
{
  const _expectType: (char: string, repeats: number) => PayloadAction<number> =
    slice.actions.reducerWithAnotherPrepareNotation;
}

Our problem here is to get consistency within that reducerWithPrepareNotation definition while also being consistent within reducerWithAnotherPrepareNotation, but having different types between both of them.

phryneas avatar Jan 02 '23 17:01 phryneas

I was experimenting with https://github.com/microsoft/TypeScript/pull/52062 and RTK types. While this PR doesn't implement this feature request here - it already allows me to do quite a lot for some libraries.

I managed to implement most of the requirements mentioned by @phryneas. I could have made a mistake here or there - but my experiment probably could have been refined with someone more intimate with RTK.

PoC RTK types with #52062
type AnyFunction = (...args: any) => any;

type PayloadAction<
  P = void,
  T extends string = string,
  M = never,
  E = never
> = {
  payload: P;
  type: T;
} & ([M] extends [never]
  ? {}
  : {
      meta: M;
    }) &
  ([E] extends [never]
    ? {}
    : {
        error: E;
      });

type PrepareMap<TPrepareMap> = {
  [K in keyof TPrepareMap]: {
    prepare?: (...args: never) => TPrepareMap[K];
  };
};

type ReducerMap<TState, TReducerMap, TPrepareMap> = {
  [K in keyof TReducerMap]:
    | ((state: TState, action: never) => void)
    | {
        reducer: (
          state: TState,
          action: TPrepareMap[K & keyof TPrepareMap] & { type: K }
        ) => void;
      };
};

export declare function createSlice<
  TState,
  TPrepareMap,
  TReducerMap,
  TFullReducers
>(arg: {
  name: string;
  initialState: TState;
  reducers: PrepareMap<TPrepareMap> &
    ReducerMap<TState, TReducerMap, TPrepareMap> & {
      [K in keyof TFullReducers]: TFullReducers[K];
    };
}): {
  actions: {
    [K in keyof TFullReducers]: TFullReducers[K] extends {
      reducer: infer R extends AnyFunction;
      prepare: infer P extends AnyFunction;
    }
      ? (...args: Parameters<P>) => Parameters<R>[1]
      : TFullReducers[K] extends infer R extends AnyFunction
      ? Parameters<R>[1] extends PayloadAction<infer P>
        ? (arg: P) => PayloadAction<P>
        : never
      : never;
  };
};

interface State {
  foo: string;
}

const slice = createSlice({
  name: "someSlice",
  initialState: {
    foo: "bar",
  } satisfies State as State,
  reducers: {
    simpleReducer: (state, action: PayloadAction<string>) => {
      state.foo = action.payload;
    },
    reducerWithPrepareNotation: {
      reducer: (state, action) => {
        state.foo = action.payload;
      },
      prepare: (char: string, repeats: number) => {
        return { payload: char.repeat(repeats) };
      },
    },
    reducerWithAnotherPrepareNotation: {
      reducer: (state, action: PayloadAction<number>) => {
        state.foo = state.foo.slice(0, action.payload);
      },
      prepare: (char: string, repeats: number) => {
        return { payload: repeats * char.length };
      },
    },
    // // uncomment to see the error. This is a "wrong user code" that we want to protect against.
    // invalidReducerWithPrepareNotation: {
    //   reducer(state, action: PayloadAction<string>) {
    //     state.foo = action.payload
    //   },
    //   prepare(char: string, repeats: number) {
    //     return { payload: repeats * char.length }
    //   }
    // },
  },
});

{
  const _expectType: (payload: string) => PayloadAction<string> =
    slice.actions.simpleReducer;
}
{
  const _expectType: (char: string, repeats: number) => PayloadAction<string> =
    slice.actions.reducerWithPrepareNotation;
}
{
  const _expectType: (char: string, repeats: number) => PayloadAction<number> =
    slice.actions.reducerWithAnotherPrepareNotation;
}

What I've learned in the process is that this feature request here would make it easier to write such types (since we would be able to "merge" TPrepareMap with TReducerMap) but it wouldn't be able to replace their "validation" logic without #52062.

The main problem is that TS often doesn't infer to type params within intersected types - which I think makes sense in most cases. So to create the return type we actually need to infer separately to TFullReducers and that can only be done using intersections and the logic from #52062.

Andarist avatar Jan 09 '23 10:01 Andarist

I asked Copilot for a summary of reverse mapped types, since I'm going to have to explain these PRs to the team. I thought it was interesting (and at least useful in terms of links)

The History and Evolution of Reverse Mapped Types in TypeScript

Introduction

Reverse mapped types represent one of TypeScript's most sophisticated type inference mechanisms, allowing the compiler to "reverse engineer" type parameters from mapped type structures. This feature enables powerful patterns for type-safe APIs, particularly in frameworks and libraries that need to infer complex type relationships.

What Are Reverse Mapped Types?

Reverse mapped types are TypeScript's ability to infer a type parameter T when you have a value that matches a mapped type structure like { [K in keyof T]: SomeType<T[K]> }. Instead of requiring explicit type annotations, TypeScript can work backwards from the provided value to determine what T must be.

Simple Example

type Mapped<T> = { [K in keyof T]: T[K] };

declare function process<T>(obj: Mapped<T>): T;

const result = process({ foo: 'hello', bar: 42 });
// T is inferred as { foo: string; bar: number; }

Historical Timeline

Early History (Pre-2018)

The foundational work for reverse mapped types was likely laid during TypeScript 2.1-2.8's development of mapped types and conditional types, though explicit reverse mapping wasn't initially supported.

2018-2019: Display and Recursion Issues

Issue #23897 (May 2018): One of the earliest documented issues with reverse mapped types involved incorrect typeToString results for nested reverse mapped types, causing display problems in tooltips and error messages.

PR #28494 (November 2018): Added ellipses (...) for reverse mapped types in nested positions to improve readability, acknowledging that these types could become unwieldy in error messages.

2019: Initial Improvements

PR #31221 (May 2019): Major improvement to reverse mapped types by Anders Hejlsberg, fixing issue #30505. This PR enhanced how TypeScript handles inference for generic function parameters through reverse mapped types, particularly for Vue.js-style component props patterns.

PR #33450 (September 2019): Fixed an issue where reverse mapped types should preserve inferable indexes from their source types.

2020-2021: Tuple Support and Instantiation

PR #41106 (October 2020): Added support for partial reverse mapped inferences with tuple types, significantly expanding the utility of reverse mapped types for variadic type patterns.

PR #42449 (April 2021): Added instantiation rules for reverse mapped types, addressing issue #42385 where deferred conditional types within reverse mapped types weren't being properly evaluated.

PR #44684 (June 2021): A DOM library update that unexpectedly affected reverse mapped type behavior due to readonly attribute additions, showing how sensitive the feature is to type system changes.

2022: Context-Sensitive Function Inference

PR #48538 (April 2022): Major enhancement allowing better inference for context-sensitive functions within object and array literal arguments. This improved left-to-right information flow between context-sensitive functions, which significantly benefited reverse mapped type patterns.

The PR addressed multiple long-standing issues (#25092, #38623, #38845, #38872, #41712, #47599, #48279, #48466) and represented a substantial leap in reverse mapped type capabilities.

2023: Intersection Constraints and Edge Cases

PR #55811 (November 2023): Revolutionary addition of reverse mapped types with intersection constraints by Andrea Simone Costa (jfet97). This PR enabled Excess Property Checking (EPC) on type parameter inference sites.

Previously, TypeScript could reverse three kinds of mapped types:

  1. Homomorphic: { [P in keyof T]: X }
  2. Type parameter constraint: { [P in K]: X } where K is a type parameter
  3. Union constraints

This PR added a fourth case: intersection constraints, allowing patterns like:

const checkType = <T>() => <U extends T>(value: { [K in keyof U & keyof T]: U[K] }) => value;

const checked = checkType<{x: number, y: string}>()({
  x: 1,
  y: "y",
  z: "z", // Error: Excess property
});

PR #55794 (November 2023): Fixed const reverse mapped types to be properly treated as const, improving literal type preservation.

PR #55941 (November 2023): Fixed an issue with reverse mapped type inference when a single type variable is left after inferring from matching types.

2024: Recursion Control and Bug Fixes

PR #57837 (April 2024): Fixed infinite recursion issues with inferReverseMappedType, preventing stack overflow errors in complex scenarios.

PR #54029 (Still Open): Proposes improvements for inference of context-sensitive functions within reverse mapped types, building on the #48538 work.

How Reverse Mapped Types Work

The Core Algorithm

At the heart of reverse mapped types is the inferReverseMappedType function in the TypeScript checker. The algorithm:

  1. Recognizes mapped type patterns in type parameter constraints
  2. Extracts the mapping relationship between keys and value types
  3. Analyzes the provided value to determine what the source type must be
  4. Constructs inferred candidates for the type parameter

Types of Reverse Mappings

1. Homomorphic Mapped Types

type Homomorphic<T> = { [K in keyof T]: T[K] };

Preserves the original structure of T.

2. Constrained Mapped Types

type Constrained<T, K extends keyof T> = { [P in K]: T[P] };

Allows partial selection of properties.

3. Union Constraint Mapped Types

type UnionConstrained<T> = { [K in keyof T | 'extra']: ...  };

Forces presence of certain properties.

4. Intersection Constraint Mapped Types (Since 5.3)

type IntersectionConstrained<T, U> = { [K in keyof T & keyof U]: T[K] };

Prevents excess properties, enabling EPC.

Key Considerations

Left-to-Right Inference: Inference flows from left to right in function arguments. Context-sensitive functions can only use information from:

  • Context-insensitive functions anywhere in the argument list
  • Context-sensitive functions in preceding positions

Const Type Parameters: The const modifier on type parameters interacts specially with reverse mapped types, affecting readonly inference and literal type preservation.

Recursion Limits: Complex recursive reverse mapped types may hit depth limits, requiring careful structuring to avoid infinite loops or excessive stack depth.

Common Patterns and Use Cases

1. Vue-Style Props Validation

type PropOptions<T> = {
  type: PropType<T>;
  validator?: (value: T) => boolean;
  default?: T;
};

type RecordPropsDefinition<T> = {
  [K in keyof T]: PropOptions<T[K]>
};

declare function defineProps<T>(props: RecordPropsDefinition<T>): T;

2. Redux-Style State Management

type ActionCreators<T> = {
  [K in keyof T]: (payload: T[K]) => { type: K; payload: T[K] }
};

declare function bindActionCreators<T>(creators: ActionCreators<T>): T;

3. Form Validation

type FieldValidator<T> = {
  [K in keyof T]: {
    value: T[K];
    validate: (val: T[K]) => boolean;
  }
};

Known Limitations and Challenges

1. Nested Context-Sensitive Functions

Inference within nested object structures containing context-sensitive functions remains challenging, particularly within reverse mapped types (Issue #52047, #53018).

2. Circular References

Self-referential types can cause inference to fail or produce never types in certain patterns (Issue #7022 references in various bugs).

3. Excess Property Checking Gaps

Prior to intersection constraints, EPC was inconsistent with reverse mapped types, leading to confusing error messages (Issues #55644, #55645, #55927).

4. Display and Tooling

Complex reverse mapped types can produce unwieldy error messages and poor IntelliSense experiences. The compiler uses special markers like ... to manage display complexity.

Recent Open Work (2023-2024)

Several PRs remain open or in backlog, indicating ongoing evolution:

  • PR #54029: Better inference for context-sensitive functions within reverse mapped types
  • PR #52062: Infer intersected reverse mapped types
  • PR #56300: Default reverse mapped type inference to its constraint
  • PR #53017: Infer type parameters from indexes on those parameters

Technical Implementation Notes

Key Source Files

  • src/compiler/checker. ts: Contains inferReverseMappedType, resolveReverseMappedTypeMembers
  • src/compiler/types.ts: Type definitions including ReverseMappedType
  • src/compiler/tracing.ts: Debugging support for reverse mapped types

Performance Considerations

Reverse mapped type resolution can be expensive. The compiler implements several optimizations:

  • Caching of resolved members
  • Early termination for unmatchable patterns
  • Recursion depth tracking to prevent runaway computation

Conclusion

Reverse mapped types represent a remarkable achievement in type system design, allowing TypeScript to maintain type safety while enabling ergonomic APIs. The feature has evolved significantly since its implicit introduction, with major milestones in tuple support (2020), context-sensitive function inference (2022), and intersection constraints (2023).

The journey from basic property mapping to sophisticated constraint-based inference with EPC demonstrates TypeScript's commitment to both power and usability. While challenges remainβ€”particularly around nested inference and display of complex typesβ€”reverse mapped types continue to be refined and expanded, enabling increasingly sophisticated type-safe patterns in modern TypeScript applications.

References


Note: This analysis is based on public GitHub history. Some internal discussions and design decisions may not be fully captured in public issues and PRs.

RyanCavanaugh avatar Dec 04 '25 21:12 RyanCavanaugh

Let me create a comprehensive list with samples for every PR mentioned in the history. I'll need to gather more details on the remaining PRs. Perfect! Now let me create a comprehensive list with samples for every PR. Here's the complete document:

Demonstrative Samples for Reverse Mapped Type PRs

Simple before/after examples showing what each PR fixed.


PR #28494 (November 2018): Use ellipses for reverse mapped types in nested positions

What it fixed: Improved display of nested reverse mapped types in tooltips and error messages to prevent showing confusing any types.

// This is a tooling/display fix, not a type-checking fix
// ❌ Before: Nested reverse mapped types displayed as 'any' in tooltips
// βœ… After: Displayed as '...' to indicate elided information

type Reducer<S> = (state: S, action: any) => S;

function combineReducers<S>(reducers: { [K in keyof S]: Reducer<S[K]> }): Reducer<S> {
  return null as any;
}

const inner = (state: string) => state;
const outer = combineReducers({ inner });
const final = combineReducers({ outer });

// Hovering over 'final' now shows cleaner type information
// Instead of: Reducer<{ outer: { inner: any } }>
// Shows: Reducer<{ outer: ...  }>

PR #31221 (May 2019): Improve reverse mapped types

What it fixed: Allowed reverse mapped types to work with partially inferable types, particularly for Vue. js-style component props.

// ❌ Before: Would not infer validator parameter type
// βœ… After: Correctly infers 'x' as MyType

interface MyType {
  valid: boolean;
}

type PropOptions<T> = {
  type: () => T;
  validator?: (x: T) => boolean;
};

type RecordPropsDefinition<T> = {
  [K in keyof T]: PropOptions<T[K]>
};

declare function defineProps<T>(props: RecordPropsDefinition<T>): T;

const result = defineProps({
  myProp: {
    type: Object as () => MyType,
    validator: (x) => {  // βœ… 'x' is now correctly typed as MyType
      return x. valid;
    }
  }
});

PR #33450 (September 2019): Reverse mapped types should have inferable indexes

What it fixed: Preserved index signature inference through reverse mapped types.

// ❌ Before: Index signature lost when inferring through reverse mapped type
// βœ… After: Index signature properly preserved

type Mapped<T> = { [K in keyof T]: { name: T[K] } };
type InferFromMapped<T> = T extends Mapped<infer R> ? R : never;

type LiteralType = {
  first: "first";
  second: "second";
};

type MappedLiteralType = {
  first: { name: "first" };
  second: { name: "second" };
};

type Inferred = InferFromMapped<MappedLiteralType>;

// βœ… Now correctly passes: Inferred is assignable to Record<string, string>
type Test = Inferred extends Record<string, string> ? true : false;
//   ^?  true (was false before)

PR #41106 (October 2020): Support partial reverse mapped inferences with tuple types

What it fixed: Made reverse mapped types work with tuples, enabling contextual typing for tuple elements.

// ❌ Before: 'arg' would have implicit 'any' type
// βœ… After: 'arg' is correctly typed from the tuple element

type Mapped<T> = {
  [K in keyof T]: [T[K], (arg: T) => boolean];
};

declare function inferMapped<T>(arg: Mapped<T>): void;

inferMapped({
  key: [3, (arg) => {
    arg.key;  // βœ… Now knows 'arg' has property 'key: number'
    return arg.key > 5;
  }],
});

PR #42449 (April 2021): Add instantiation rules for reverse mapped types

What it fixed: Deferred conditional types within reverse mapped types now evaluate properly when type parameters are known.

// ❌ Before: Conditional type stayed deferred, didn't evaluate
// βœ… After: Conditional type evaluates to correct branch

interface Targets<A> {
  left: A;
  right: A;
}

type Target = keyof Targets<any>;
type Result<F extends Target, A> = Targets<A>[F];

interface Ops<F extends Target> {
  dict: <P>(p: { [k in keyof P]: Result<F, P[k]> }) => Result<F, P>;
}

declare const left: Ops<"left">;
declare const right: Ops<"right">;

// Simple function that works
const simple = <F extends Target>(at: Ops<F>) => ({
  value: "hello" as Result<F, string>
});

// Function using dict - this is what was broken
const withDict = <F extends Target>(at: Ops<F>) => at. dict(simple(at));

const leftResult = withDict(left);
//    ^? { value: string } βœ… (was broken before - conditional didn't evaluate)

const rightResult = withDict(right);
//    ^? { value: string } βœ…

PR #48538 (April 2022): Improved function inference in objects and methods

What it fixed: Inference between context-sensitive functions in object literals now flows left-to-right, just like for discrete arguments.

// ❌ Before: Second function's parameter wasn't inferred
// βœ… After: Information flows from first to second function

declare function process<T>(arg: { 
  produce: (n: number) => T;
  consume: (x: T) => void;
}): void;

// All of these now work:
process({
  produce: () => 42,           // Context-insensitive
  consume: x => x.toFixed()    // βœ… 'x' inferred as number
});

process({
  produce: n => n,             // Context-sensitive (infers return from 'n')
  consume: x => x.toFixed()    // βœ… 'x' inferred as number (was error before)
});

process({
  produce: function() { return 42 },  // Function expression
  consume: x => x. toFixed()           // βœ… 'x' inferred as number (was error before)
});

process({
  produce() { return 42 },     // Method shorthand
  consume: x => x.toFixed()    // βœ… 'x' inferred as number (was error before)
});

PR #55794 (November 2023): Fixed const reverse mapped types

What it fixed: Reverse mapped types with const type parameters now properly preserve readonly and literal types.

// ❌ Before: Literals widened, readonly lost
// βœ… After: Literals preserved, readonly maintained

declare function process<const T extends readonly unknown[]>(
  args: { [K in keyof T]: T[K] }
): T;

const result = process(["hello", 42] as const);
//    ^?  readonly ["hello", 42]  βœ… (was: (string | number)[] before)

// Also works for objects
declare function processObj<const T>(
  obj: { [K in keyof T]: T[K] }
): T;

const obj = processObj({ x: 1, y: "hello" } as const);
//    ^? { readonly x: 1; readonly y: "hello" }  βœ…

PR #55811 (November 2023): Reverse mapped types with intersection constraint

What it fixed: Added intersection constraints to enable Excess Property Checking (EPC) on inferred types.

// ❌ Before: Extra properties silently accepted
// βœ… After: Excess properties caught by EPC

interface Foo {
  bar: number;
  baz: string;
}

// Intersection constraint: keyof T & keyof Foo
declare function useFoo<T extends Foo>(
  s: { [K in keyof T & keyof Foo]: T[K] }
): T;

useFoo({ 
  bar: 1,
  baz: 'hello',
  extra: 123  // βœ… Error: Object literal may only specify known properties
});

// Also works with curried functions:
const checkType = <T>() => 
  <U extends T>(value: { [K in keyof U & keyof T]: U[K] }) => value;

const checked = checkType<{ x: number; y: string }>()({
  x: 1,
  y: "y",
  z: "z",  // βœ… Error: 'z' does not exist in type
});

PR #55941 (November 2023): Fixed reverse mapped types inference with single type variable

What it fixed: Corrected inference when a single type variable remains after inferring from matching types.

// ❌ Before: Inference failed or produced incorrect results
// βœ… After: Single remaining type variable infers correctly

type MappedValue<T> = {
  [K in keyof T]: { value: T[K] };
};

declare function process<T>(arg: MappedValue<T>): T;

// When there's only one property left to infer from
const result = process({
  single: { value: 42 }
});
//    ^? { single: number } βœ… (was broken in certain edge cases)

// More complex case that was failing
declare function transform<A, B>(
  input: { a: MappedValue<A>; b: B }
): [A, B];

const transformed = transform({
  a: { prop: { value: "hello" } },
  b: 123
});
//    ^? [{ prop: string }, number] βœ…

PR #57837 (April 2024): Avoid infinite recursion with inferReverseMappedType

What it fixed: Prevented stack overflow errors with deeply recursive reverse mapped types.

// This is a crash fix - no demonstrative sample needed
// Previously would cause: "Error: Maximum call stack size exceeded"
// Now: Properly handles recursion with cutoff logic

// Example that previously crashed:
type DeepRecursive<T> = T extends object ?  {
  [K in keyof T]: DeepRecursive<T[K]>
} : T;

declare function process<T>(arg: DeepRecursive<T>): T;

// This would previously crash the compiler with stack overflow
// Now it properly handles the recursion depth
const result = process({
  level1: {
    level2: {
      level3: {
        level4: {
          value: 42
        }
      }
    }
  }
});

Summary Table

PR Date What It Fixed
#28494 Nov 2018 Display: Use ... instead of any for nested reverse mapped types
#31221 May 2019 Partial inference for Vue-style props validation
#33450 Sep 2019 Preserve index signatures through reverse mapped types
#41106 Oct 2020 Contextual typing for functions in tuple elements
#42449 Apr 2021 Evaluate deferred conditionals within reverse mapped types
#48538 Apr 2022 Left-to-right inference for functions in object literals
#55794 Nov 2023 Preserve const modifiers (readonly, literals)
#55811 Nov 2023 Intersection constraints for excess property checking
#55941 Nov 2023 Single type variable edge case after matching
#57837 Apr 2024 Prevent stack overflow in deep recursion

Each sample is minimal and focuses on the specific problem that was solved, making it easy to understand the progression of the feature over time.

RyanCavanaugh avatar Dec 04 '25 21:12 RyanCavanaugh