This Crazy Syntax Lets You Get An Array Element's Type
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
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.
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.
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'.2322Type '(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.
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.
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!
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.
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.
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.
Share this article with your friends
Learn how to extract the type of an array element in TypeScript using the powerful Array[number]
trick.
Learn how to publish a package to npm with a complete setup including, TypeScript, Prettier, Vitest, GitHub Actions, and versioning with Changesets.
Enums in TypeScript can be confusing, with differences between numeric and string enums causing unexpected behaviors.
Is TypeScript just a linter? No, but yes.
It's a massive ship day. We're launching a free TypeScript book, new course, giveaway, price cut, and sale.
Learn why the order you specify object properties in TypeScript matters and how it can affect type inference in your functions.