TypeScript: Explicit vs Implicit type annotation πŸ€”

Using TypeScript can provide many benefits, such as reducing bugs or self-documenting codebase. But it can also cause dilemmas, such as whether to allow TypeScript to infer types or to explicitly annotate the static type, as in other programming languages.

TypeScript: Explicit vs Implicit type annotation πŸ€”
Photo by Joan Gamell / Unsplash

To better understand our concern, that we have, we need to distinguish between two possibilities for adding type annotations in TypeScript: explicit and implicit.

Today I will try to deal with these two type annotations and indicate when the explicit type is desirable and when to rely on type inference. If you don't know what Type Inference is, please read the handbook.

Explicit & Implicit

Firstly, let's distinguish between them:

Explicit – means adding a type directly to our codebase, similar to Haskell language, which is statically typed. If we add such a type, we will know exactly what type we are using, e.g :

const animal: string = 'Tiger';

If we want to be more specific, we can describe this variable as const and β€œseal” the type:

const animal = 'Tiger' as const;

In this case, the type of the animal variable will be strict – ⁣Tiger.

Implicit – means the type can be inferred by the TypeScript inference system when there is no explicit type annotation:

const animal = 'Tiger';

In the above code, the type will be the same as in the as const example, and you can see it by hovering over the variable in your IDE. But the situation changed when we change the keyword to, let and the type then will be simple string:

let animal = 'Tiger';

Case studies πŸ’‘

Knowing the differences, let’s go over a few use cases where adding an explicit type annotation will be helpful and won't obscure your codebase.

The first thing that many new TypeScript developers do when they convert a codebase from JavaScript is fill it with type annotations. TypeScript is about types, after all! But in TypeScript many annotations are unnecessary. Declaring types for all your variables is counterproductive and is considered poor style. Effective TypeScript
  1. Passing arguments – sometimes adding an explicit type annotation is desirable, when TypeScript does not have enough context to infer the types e.g., function parameters:
const getAnimal = (animal) => animal;

In that case, the animal will be of any type, to fix it, we can either specify an explicit type here or set the default value::

const getAnimal = (animal = 'Elephant') => animal;

2. Faster error catching – sometimes, explicit type annotation can save us time debugging and finding an error. Let's look at this example:

interface Animal {
  id: number;
  name: string;
}

const animal = {
  id: '1',
  name: 'Elephant',
};

const getAnimal = (animal: Animal) => animal;

getAnimal(animal);

This code throws an error at the place where we call the getAnimal function. This is the appropriate behavior, because TypeScript will infer the type as an object with the properties of types inferred from the given values:

However, if we know that this variable should be of type Animal, then when we annotate the type explicitly, we immediately get an error in the place where we're pass the wrong type:

3. Explicit help infer types – that is kind of a paradox, but every time we add an explicit type, we're helping TypeScript infer types. Having correctly typed in/out function data, we can omit adding the type in body function and based on the type inference:

ESLint/ TypeScript ESLint

To complete the above list, to make it easier to work with TypeScript, Β we are going to need a utility called linter (in this case the most popular – ESLint). To work with TypeScript, there is a dedicated linter – TypeScript ESLint, which allows ESLint to run on TypeScript code.

There is a tremendous number of rules, but I will point up 3 that are really helpful for working with explicit/implicit types.

  1. @typescript-eslint/typedef - this rule find in the code places where we don't have provided a type definition
TypeScript cannot always infer types for all places in code. Some locations require type annotations for their types to be inferred

This rule can be nicely configured with two flags to suit to our needs and work with incoming data into functions/methods:

"@typescript-eslint/typedef": [
	"error",
    { 
        "arrowParameter": true, 
        "parameter": true 
    }
]

However, it is also a good approach to enable rules in the tsconfig.json file, as the documentation says:

Instead of enabling typedef, it is generally recommended to use the --noImplicitAny and --strictPropertyInitialization compiler options to enforce type annotations only when useful.

2. @typescript-eslint/explicit-function-return-type – this rule forces an explicit addition to be added to the function. Adding an explicit return type gives you one more privilege – it allows you to use of a defined type's declaration and have more control over what type of β€œout” data is.

3. @typescript-eslint/no-inferrable-types – the last rule finds an unnecessary added explicit type at places where types might be inferred:

Conclusion:

There are advantages and disadvantages to adding explicit type annotation, but in the long run, adding in some places is the desired thing, which makes your code understandable, consistent, and forces you to think in type-first.

β€œIdeal TypeScript code includes type annotations for function/method signatures but not for the local variables created in their bodies. This keeps noise to a minimum and lets readers focus on the implementation logic”. Effective TypeScript

Thanks for reading β™₯️β™₯️

If this article was helpful, please leave a comment or πŸ‘