Home > Back-end >  Wildcard return type in Typescript where objects that implement an interface are returned
Wildcard return type in Typescript where objects that implement an interface are returned

Time:06-29

I have the following interface that I use in an abstract base class:

interface MyInterface {
   field1: string
}

abstract class BaseClass {
   // some fields here...
   abstract serialize(): Array<MyInterface>
}

I now have a several subclasses that extend from BaseClass that each contain a couple of additional fields that are different per subclass. E.g.

class Subclass1 extends BaseClass {
   // some additional fields that are not in BaseClass
   serialize(): Array<MyInterface> {
      return {field1: "test", field2: "test2"};
   }
}

Now I am looking for the correct return type of the serialize function that should have the semantics of:

"return any object that implements MyInterface, i.e. that has at least all fields from MyInterface, but it could also have more"

In the above setup, Typescript is giving me an error because the return type does not conform to MyInterface. This also makes sense. field2 is not in the interface.

I think I am looking for the Typescript equivalent of a Java unbounded wildcard. Sth. like

List<? extends MyInterface>

meaning anything that implements MyInterface

CodePudding user response:

You're not even returning an array in your example, which is your principle problem..

However, if you directly try to return the array:

return [{field1: "test", field2: "test2"}];

you will fall victim to excess property checks. (see also).

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error

It's actually perfectly acceptable to return something with a wider surface area that conforms to the return-type, but not if you create it on-the-spot. TypeScript takes the view that if the only existing reference to an object has excess properties then they are there by mistake because they become immediately inaccessible.

So... although I wouldn't recommend directly returning an object literal with properties that are non-retrivable unless you use type-assertions, you can sidestep by assigning an intermediate variable and return that instead.

// unuseful way of sidestepping
// excess property checks. bad idea.
const returnValue = [{field1: "test", field2: "test2"}];
return returnValue;

In all likelihood, in the real-world, the thing that you are trying to return is already stored in a variable or field and this problem stems from simplistic test/example code.

Manufacturing an object of the wrong shape specifically to return something that conforms to an interface with a narrower surface area smells of a mistake, so TypeScript actively tries to prevent it.

Playground Link

CodePudding user response:

It sounds like you just need to intersect MyInterface with the extra fields you want to return. Subclasses can refine their methods return types in this way so long as the return type still conforms to the base class.

class Subclass1 extends BaseClass {
   serialize(): Array<MyInterface & { field2: string }> {
      return [{field1: "test", field2: "test2"}]
   }
}

const foo = new Subclass1().serialize()
// (MyInterface & { field2: string })[]

See playground


Or if you want BaseClass to enforce this, you can make it generic, and have the extra props type merged there. This cleans things up a bit, and make it clear at the top level of the class what other data this class might deal with.

Now you can remove the return type annotation entirely since it's completely enforced by BaseClass.

interface MyInterface { field1: string }

abstract class BaseClass<Extra = Record<string, never>> {
   abstract serialize(): Array<MyInterface & Extra>
}

class Subclass1 extends BaseClass<{ field2: string }> {
   serialize() {
      return [{field1: "test", field2: "test2"}]
   }
}

const foo = new Subclass1().serialize()
// { field1: string, field2: string }[]

NOTE: Record<string, never> looks weird, but it basically means an empty object, which is a sensible default for this generic type parameter.

See Playground

  • Related