Home > other >  Is there a way to express interface function polymorphism when the function will always receive the
Is there a way to express interface function polymorphism when the function will always receive the

Time:01-10

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!

CodePudding user response:

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

  • Related