3

Trying to force a method with a generic parameter to have a union type. The issue is that TS is fine with the implementation of this method to only satisfy the union, rather than actually be the union. I might be misusing the satisfy terminology here, but I hope my point comes across clearly in the example below.

interface IFoo<TArg> {
  myMethod(arg: TArg): void;
}

class Foo implements IFoo<string | number> {
  // TS doesn't complain because string satisfies string | number.
  // How can I make TypeScript force arg to explicitly be string | number?
  myMethod(arg: string): void {
    //
  }
}


class Foo2 implements IFoo<string | number> {
  // What I want TS to enforce:
  myMethod(arg: string | number): void {
    //
  }
}

I messed around with tuples and tried looking for a helper type that could solve this to no avail.

edit: Link to playground: https://tsplay.dev/wOx5EN

alexweininger
  • 130
  • 1
  • 6
  • Huh, yeah this seems not ideal: https://tsplay.dev/mp8xXm. Or more simply: https://tsplay.dev/m33v2m – Alex Wayne Dec 15 '22 at 23:15
  • 1
    Although when using the type directly (as opposed to `implements`) leaving the argument type off does the right thing. https://tsplay.dev/wXzrDm – Alex Wayne Dec 15 '22 at 23:18

1 Answers1

2

You're running up against the fact that method parameters are bivariant in TypeScript. That means you are allowed to narrow the type of a method parameter when implementing an interface, like narrowing string | number to just string. That's not type safe (what if someone calls myMethod() with a number argument?), but it is convenient for some important use cases (you could also see this FAQ entry for why).

Now, this used to be the case for all functions, but since the --strictFunctionTypes compiler flag was introduced, you can restrict the bivariance check to just methods, while other function types would have their parameters checked in the safe contravariant way. (You can look at Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more information.)

That means one way you can sidestep the issue is to declare myMethod() in IFoo<TArg> as a property with a function type instead of a method type:

interface IFoo<TArg> {
  myMethod: (arg: TArg) => void;
}

It might look like that would force implementing classes to make myMethod an instance property instead of a method, but that's not the case. So this works just fine:

class Foo2 implements IFoo<string | number> {
  myMethod(arg: string | number): void { // okay
    //
  }
}

But even if you implement it as a method, the parameter is now checked contravariantly against the interface function property parameter:

class Foo implements IFoo<string | number> {
  myMethod(arg: string): void { // error!!!
    //
  }
}

and you get the error you were looking for.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360