skip to Main Content

I want to have an interface where I know the parameter is going to be 1 of a few different types (coming from a 3rd party library) and based on what type it is, the return type will change, with some types also being from a 3rd party library, or being a basic type like void or null.

Here is the simplest code to replicate my issue

type foo = {
  statusCode: number;
  body: string;
}
type fooReturn = string; // in reality, this could be a complex type

type bar = {
  results: [];
}
type barReturn = number; // in reality, this could be a complex type

interface foobar {
  myFunc<T extends (foo | bar)>(event: T): T extends foo ? fooReturn : barReturn;
}

class myClass implements foobar {
  // the below code errors
  myFunc(event: foo) { // we tell TS that the param is of type foo
    return 'hello' // so the return must be a string
  }
}

The error is

Property 'myFunc' in type 'myClass' is not assignable to the same property in base type 'foobar'.
  Type '(event: foo) => string' is not assignable to type '(event: T) => T extends foo ? string : number'.
    Types of parameters 'event' and 'event' are incompatible.
      Type 'T' is not assignable to type 'foo'.
        Type 'foo | bar' is not assignable to type 'foo'.
          Type 'bar' is missing the following properties from type 'foo': statusCode, body

2

Answers


  1. I think the correct way to handle this is by using Function Overloads. Then you can use a type guard to narrow the event parameter and return the correct type.

    type Foo = {
      statusCode: number;
      body: string;
    };
    type FooReturn = string;
    
    type Bar = {
      results: [];
    };
    type BarReturn = number;
    
    interface FooBar {
      myFunc(event: Foo): FooReturn;
      myFunc(event: Bar): BarReturn;
      myFunc(event: Foo | Bar): FooReturn | BarReturn;
    }
    
    class myClass implements FooBar {
      myFunc(event: Foo): FooReturn;
      myFunc(event: Bar): BarReturn;
      myFunc(event: Foo | Bar): FooReturn | BarReturn{
        if ("results" in event) {
          return 666;
        } else {
          return "hello";
        }
      }
    }
    

    TypeScript Playground

    Login or Signup to reply.
  2. The problem here seems to be that you’ve incorrectly scoped the generic type parameter for your intent. The type

    interface Foobar {
        myFunc<T extends Foo | Bar>(event: T): T extends Foo ? FooReturn : BarReturn;
    }
    

    is a specific type with a generic method. Generic methods have the generic type parameter scoped to the call signature, meaning the caller decides what type argument goes in there. If I want to implement a Foobar, then its myFunc() method has to allow the caller to call with whatever T they want. It must support both myFunc(event: Foo): FooReturn and myFunc(event: Bar): BarReturn. But MyClass does not implement that correctly; it only supports Foo and not Bar.

    Instead, you probably want

    interface Foobar<T extends Foo | Bar> {
        myFunc(event: T): T extends Foo ? FooReturn : BarReturn;
    }
    

    which is a generic type, with a specific method. Generic types have the generic type parameter scoped to the type itself, meaning that the implementer decides what type argument goes in there. With that definition, you need to decide whether you’re implementing Foobar<Foo> or Foobar<Bar>. Once you make that decision, the type argument for T is chosen and fixed. The call signature for myFunc() isn’t generic. And your MyClass does properly implement Foobar<Foo>:

    class MyClass implements Foobar<Foo> {
        myFunc(event: Foo) { // okay
            return 'hello'
        }
    }
    

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search