kolodny / immutability-helper

mutate a copy of data without changing the original source
MIT License
5.17k stars 186 forks source link

TS2589: Type instantiation is excessively deep and possibly infinite #157

Open MichaelJLiu opened 4 years ago

MichaelJLiu commented 4 years ago

After upgrading from TypeScript 3.5 to 3.8, I started getting error TS2589 on code that calls update with a Spec parameter. Here's a repro:

import update, { Spec } from "immutability-helper";

interface Widget {
    name: string;
}

function f(widget: Widget, spec: Spec<Widget>): Widget {
    return update(widget, spec); // <== TS2589
}

Compiler output:

repro.ts:8:9 - error TS2589: Type instantiation is excessively deep and possibly infinite.

8  return update(widget, spec);
          ~~~~~~~~~~~~~~~~~~~~

The error seems to occur only when strict null checks are enabled.

Is there something wrong with my code? Or is this a new limitation of the TypeScript compiler?

kolodny commented 4 years ago

Is this the same as https://github.com/kolodny/immutability-helper/issues/150?

MichaelJLiu commented 4 years ago

As suggested in #150, adding an explicit type argument resolves the problem:

return update<Widget>(widget, spec); // no more TS2589

Does anyone know why recent versions of TypeScript can no longer infer the type?

kolodny commented 4 years ago

I think I understand what's happening. Usually when you pass in a spec object, it resolves to a spec and doesn't need to keep what the spec can be open ended. The issue is when you try to type that arg as a spec there's a recursive definition of Spec.

This also shows up when trying to just type that arg as Spec

update(1, {$set: 2}); // ok
update(1, {$set: 2} as Spec<any>); // Type instantiation is excessively deep and possibly

This was probably an issue in previous TS version, however, I believe it was more permissive on issues like that and didn't consider them an error

MichaelJLiu commented 4 years ago

immutability-helper/index.d.ts declares update as follows:

declare const _default: <T, C extends CustomCommands<object> = never>(object: T, $spec: Spec<T, C>) => T;
export default _default;

If I call update with arguments of type Widget and Spec<Widget>, I don't understand why TypeScript can't immediately infer that T is Widget.

But in any case, can anything be done to avoid having to specify an explicit type argument, either in immutability-helper or in the TypeScript compiler? Or should this just be documented as a known limitation?