All Articles

Method Shorthand Syntax Considered Harmful

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

You might have noticed that there are two ways you can annotate a function on an object in TypeScript.

interface Obj {
  // Method shorthand syntax
  version1(param: string): void;
  // Object property syntax
  version2: (param: string) => void;
}

They look very innocuous. But there's a subtle difference between the two. And thanks to a tweet from my friend Andarist, I can now say that method shorthand syntax should be avoided in almost all cases.

Why Is Method Shorthand Syntax Bad?

Using the method shorthand syntax can result in runtime errors. This is for a complicated reason, but I'll try to explain it as best as I can.

Let's say we declare a Dog interface with a barkAt method.

interface Dog {
  barkAt(dog: Dog): void;
}

It turns out that when you use Dog to type a variable, you can annotate the parameter for .barkAt with a narrower type than the Dog interface expects:

interface SmallDog extends Dog {
  // Only small dogs whimper in this universe
  whimper: () => void;
}

const brian: Dog = {
  barkAt(dog: SmallDog) {},
};

Here, we've made it so brian only wants to bark at small dogs, which have an extra .whimper() method.

This might look fine, but we're actually on the verge of a runtime error that TypeScript won't catch. Inside brian's barkAt function we could easily call dog.whimper().

const brian: Dog = {
  barkAt(smallDog: SmallDog) {
    smallDog.whimper();
  },
};

Then, we could declare a new dog - just a normal one without a whimper method:

const normalDog: Dog = {
  barkAt() {},
};

But when we pass the normal dog to brian.barkAt, it will fail at runtime:

brian.barkAt(normalDog); // runtime error here!

It'll try to call .whimper() on normalDog, which doesn't exist. And our app will blow up.

So this is TypeScript failing to prevent a runtime error. And it's all because of the method shorthand syntax.

How Do We Fix This?

If we use object property syntax to define the method, TypeScript will throw an error if we try to assign a narrower type to the method:

interface Dog {
  // 1. We change it to an object property syntax...
  barkAt: (dog: Dog) => void;
}

interface SmallDog extends Dog {
  whimper: () => void;
}

const brian: Dog = {
  // 2. ...and now it errors!
  barkAt(dog: SmallDog) {},
Type '(dog: SmallDog) => void' is not assignable to type '(dog: Dog) => void'. Types of parameters 'dog' and 'dog' are incompatible. Property 'whimper' is missing in type 'Dog' but required in type 'SmallDog'.2322
Type '(dog: SmallDog) => void' is not assignable to type '(dog: Dog) => void'. Types of parameters 'dog' and 'dog' are incompatible. Property 'whimper' is missing in type 'Dog' but required in type 'SmallDog'.};

This is more in line with what we expect.

Is It Something To Do With Arrow Functions?

A common misconception is that the syntax above refers to arrow functions vs function declarations. This is not the case. Both syntaxes can be used with arrow functions or function declarations.

interface Obj {
  methodShorthand(param: string): void;
  objectProperty: (param: string) => void;
}

function functionDeclaration(param: string) {}
const arrowFunction = (param: string) => {};

const examples: Obj[] = [
  {
    // You can pass arrow functions to method shorthands...
    methodShorthand: arrowFunction,
    // ...and vice versa
    objectProperty: functionDeclaration,
  },
  {
    methodShorthand: functionDeclaration,
    objectProperty: arrowFunction,
  },
];

As you can see, the syntax does not restrict whether you can use a function declaration or arrow function.

Would This Work With Types Instead Of Interfaces?

I've used interface in the example above, but the same problem occurs with type:

type Dog = {
  barkAt(dog: Dog): void;
};

type SmallDog = {
  whimper: () => void;
} & Dog;

const brian: Dog = {
  barkAt(smallDog: SmallDog) {
    smallDog.whimper();
  },
};

const normalDog: Dog = {
  barkAt() {},
};

brian.barkAt(normalDog); // runtime error here!

Why Does This Happen?

This happens because the method shorthand syntax is bivariant. This means that the method can accept a type that is both narrower and wider than the original type.

This is not the case with the object property syntax. It only accepts a type that is narrower than the original type.

It's this unexpected bivariance that can lead to runtime errors.

The ESLint Rule

If you want to avoid this problem, you can use the ESLint rule @typescript-eslint/method-signature-style. This rule will enforce the use of the object property syntax for method signatures.

{
  "rules": {
    "@typescript-eslint/method-signature-style": [
      "error",
      "property"
    ]
  }
}

So, if you're seeing an error like...

Shorthand method signature is forbidden. Use a function property instead.

...it's because you're using the method shorthand syntax, and some clever person has set up this rule to prevent you from doing so.

Are There Any Use Cases?

The reason this exists in TypeScript is because, very occasionally, you want to allow bivarance on function declarations. I'm going to cop to the fact that I'm not quite sure what those reasons are.

Michael Arnaldi, one of the authors of EffectTS, seemed to have a good read on the situation in this thread.

Matt's signature

Share this article with your friends