How To Strongly Type process.env
Learn how to strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
You might have noticed that there are two ways you can annotate a function on an object in TypeScript.
ts
interfaceObj {// Method shorthand syntaxversion1 (param : string): void;// Object property syntaxversion2 : (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.
ts
interfaceDog {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:
ts
interfaceSmallDog extendsDog {// Only small dogs whimper in this universewhimper : () => void;}constbrian :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()
.
ts
constbrian :Dog = {barkAt (smallDog :SmallDog ) {smallDog .whimper ();},};
Then, we could declare a new dog - just a normal one without a whimper
method:
ts
constnormalDog :Dog = {barkAt () {},};
But when we pass the normal dog to brian.barkAt
, it will fail at runtime:
ts
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:
ts
interfaceDog {// 1. We change it to an object property syntax...barkAt : (dog :Dog ) => void;}interfaceSmallDog extendsDog {whimper : () => void;}constbrian :Dog = {// 2. ...and now it errors!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'.( barkAt dog :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.
ts
interfaceObj {methodShorthand (param : string): void;objectProperty : (param : string) => void;}functionfunctionDeclaration (param : string) {}constarrowFunction = (param : string) => {};constexamples :Obj [] = [{// You can pass arrow functions to method shorthands...methodShorthand :arrowFunction ,// ...and vice versaobjectProperty :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
:
ts
typeDog = {barkAt (dog :Dog ): void;};typeSmallDog = {whimper : () => void;} &Dog ;constbrian :Dog = {barkAt (smallDog :SmallDog ) {smallDog .whimper ();},};constnormalDog :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.
json
{"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 strongly type process.env in TypeScript by either augmenting global type or validating it at runtime with t3-env.
Discover when it's appropriate to use TypeScript's any
type despite its risks. Learn about legitimate cases where any
is necessary.
Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.
Improve React TypeScript performance by replacing type & with interface extends. Boost IDE and tsc speed significantly.
In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.
Learn how TypeScript's new utility type, NoInfer, can improve inference behavior by controlling where types are inferred in generic functions.