Search code examples
typescriptpolymorphismcovariance

Is there a way to express interface function polymorphism when the function will always receive the type it's defined on?


I was hoping to define an interface hierarchy, where the base interface declares a function, and each extension's version of that function receives its own type (rather than the base type). Minimally, I tried:

interface IBase {
  a: string,
  f: (x: IBase) => any // Cause of the problem
}

interface IExtension extends IBase {
  b: string,
}

const f1 = (x: IExtension) => //... typechecks when using x.b

const ext1: IExtension = {
  a: "a1",
  b: "b1",
  f: f1 // This line doesn't typecheck because IExtension is not strictly IBase
}
  

The type error:

Type '(x: IExtension) => {}[]' is not assignable to type '(x: IBase) => any'

Digging around, I saw this answer regarding strictFunctionTypes. Making the following changes causes the program to typecheck, because methods aren't subject to strictFunctionTypes, and therefore allow bivariance:

interface IBase {
  a: string,
  f(x: IBase): any
}

Edit: as explained in the comment by @jcalz, this approach is blatantly unsound. It also doesn't capture the constraint that f is called with the type it's defined on.

Is there a way express this typing in TypeScript? Something like:

interface IBase {
  a: string,
  f: (x: IBase | * extends IBase) => any
}

I haven't been able to find anything like that that avoids generics. I understand that generics could be used here, but I won't be walking that route, especially given the method syntax works as expected. Really appreciate any additional insight on this topic!


Solution

  • It looks like you want to use the polymorphic this type, which is sort of an implicit generic type referring to the current type:

    interface IBase {
      a: string,
      f: (x: this) => any
      //     ^^^^
    }
    

    Then when you extend IBase, the f method of the extensions will automatically refer to the extensions and not IBase:

    interface IExtension extends IBase {
      b: string,
    }
    
    type IXF = IExtension['f'];
    // type IXF = (x: IExtension) => any
    
    const ext1: IExtension = {
      a: "a1",
      b: "b1",
      f: x => x.b.toUpperCase()
    }
    

    and

    interface ISomethingElse extends IBase {
      z: number
    }
    
    type ISF = ISomethingElse['f']
    // type ISF = (x: ISomethingElse) => any
    
    const sth2: ISomethingElse = {
      a: "a2",
      f: s => s.z.toFixed(),
      z: 123
    }
    

    Playground link to code