#Changing to property signature causes incorrect type inference with lambda and generic type

17 messages · Page 1 of 1 (latest)

edgy steeple
#

We are an academic group maintaining a TS project. We decided to enforce ESLint rules recently and one of the rules suggested us to change from shorthand method signature (func(arg0: object): number) to (func: (arg0: object) => number) .

I know the following description will be confusing, I attempted to create a minimal reproduction, but attempts were futile as I could not reproduce the issue where generic type could not be correctly inferred.

(This is the permalink of the repo/code where the issue arises)

In short, we have a class and a member function with this signature:

export class Bank<T extends Reactor, S> {
  private readonly members = new Array<T>();
  //......
  public port<P extends Port<unknown> | MultiPort<unknown>>(selector: (reactor: T) => P): P[] {
    return this.members.reduce((acc, val) => acc.concat(selector(val)), new Array<P>(0));
  }
}

And this interface. which we changed according to ESLint suggestion:

interface IOPortManager<T> extends TriggerManager {
  // Version A
-  addReceiver(port: WritablePort<T>): void;
  // Version B
+  addReceiver: (port: WritablePort<T>) => void;
}

We have a test that goes like this:

class MultiPeriodic extends Reactor {
  o = new OutMultiPort<number>(this, 2);
  //...
}
const d = new Bank<MultiPeriodic>(/*...*/);
// d.members is MultiPeriodic[]
const func = (member: MultiPeriodic) => member.o;
this.d.port(func); // Errors

If we have the interface with version A declaration we have no issue. If we have version B, then the error message is:
[1/2]

GitHub

A reactor-oriented programming framework in TypeScript - reactor-ts/tests/bank.ts at 6b782a46d756faa68e4e6962b2a5eb9c0ebece59 · lf-lang/reactor-ts

dusky loomBOT
#
const allWriter = this.d.allWritable(this.d.port((member) => member.o));
edgy steeple
#
    __tests__/bank.ts:66:19 - error TS2345: Argument of type '(member: MultiPeriodic) => OutMultiPort<number>' is not assignable to parameter of type '(reactor: MultiPeriodic) => Port<unknown> | MultiPort<unknown>'.
      Type 'OutMultiPort<number>' is not assignable to type 'Port<unknown> | MultiPort<unknown>'.
        Type 'OutMultiPort<number>' is not assignable to type 'MultiPort<unknown>'.
          The types returned by 'channels()' are incompatible between these types.
            Type 'OutPort<number>[]' is not assignable to type 'IOPort<unknown>[]'.
              Type 'OutPort<number>' is not assignable to type 'IOPort<unknown>'.
                The types of 'asWritable(...).getPort().getManager(...).addReceiver' are incompatible between these types.
                  Type '(port: WritablePort<number>) => void' is not assignable to type '(port: WritablePort<unknown>) => void'.
                    Types of parameters 'port' and 'port' are incompatible.
                      Type 'WritablePort<unknown>' is not assignable to type 'WritablePort<number>'.
                        The types returned by 'get()' are incompatible between these types.
                          Type 'unknown' is not assignable to type 'number | undefined'.

    66       this.d.port(func);

I understand that I am not supposed to assign unknown to any other type before narrowing. What confuses me is this part:

Type '(port: WritablePort<number>) => void' is not assignable to type '(port: WritablePort<unknown>) => void'.
  [...]
    Type 'WritablePort<unknown>' is not assignable to type 'WritablePort<number>'.

Why is the signature reversed? Is it because that only if they can assign both ways one can assert that this assignment is provably sound, as stressed here ?

Apologies for such a long question and not being able to provide a consice minimal reproduction. Many thanks! [2/2]

GitHub

With this PR we introduce a --strictFunctionTypes mode in which function type parameter positions are checked contravariantly instead of bivariantly. The stricter checking applies to all function t...

#

PS: We are on TS 5.1.6 and 4.9.5.

desert jetty
#

i think you know this already, but the method syntax makes functions that are unsafely bivariant in their parameters, while the function-as-property syntax is correctly contravariant in its parameters

#

that means that (x: Animal) => void is assignable to (x: Dog) => void (if Dog is a subtype of Animal). an (x: Animal) => void can handle any Dog passed to it, but not necessarily the other way around. both directions are allowed if you use the method syntax, but that means a (x: Dog): void method can end up being unsafely called with a Cat or any other subtype of Animal

#

anyway, applying this to your situation: WritablePort<number> is a subtype of WritablePort<unknown>, so similar to above, (port: WritablePort<unknown>) => void is assignable to (port: WritablePort<number>) => void, not the other way around

dusky loomBOT
#
export abstract class WritablePort<T> implements ReadWrite<T> {
  abstract get(): T | undefined;
  abstract set(value: T): void;
  abstract getPort(): IOPort<T>;
}
desert jetty
desert jetty
edgy steeple
#

Thanks! That appears to be the case then. We tried to let the function infer the correct generic type by using signature

public port<P extends Port<V> | MultiPort<V>, V>(selector: (reactor: T) => P): P[]

But it appears that it still infers the generic types to be <Multiport<unknown>, unknown>, unless I call it specfically with

d.port<OutMultiPort<number>, number>(func);

Sorry for the continued question, but if you happen to know any ways to design the function signature so that TS could infer the types correctly I would be most grateful.

desert jetty
#

complete shot in the dark until i get a chance to dig into your codebase more, have you tried anything along these lines?

public port<F extends (reactor: Port<V> | MultiPort<V>) => unknown, V>(selector: F): ReturnType<F>[]
#

oops, sorry i swapped parameters and return type

#

more like this, i think:

public port<F extends (reactor: T) => Port<V> | MultiPort<V>, V>(selector: F): ReturnType<F>[]
edgy steeple
#

Really thanks for the help!
Unfortunately this fix is not working as intended; aside from issues that arise from TS not being able to derive that ReturnType<F> is of type T extends Port<V> | MultiPort<V> and array concat is banned by tsc, calling port with the original func still result in V being unknown.

I fear that this is a result of our suboptimal type design as outlined by Ryan here: https://github.com/microsoft/TypeScript/issues/40855; hopefully there's some good solution to bypass it, but thanks for the help again!

GitHub

TypeScript is a superset of JavaScript that compiles to clean JavaScript output. - Issues · microsoft/TypeScript