Codementor Events

Prefer strict types in Typescript

Published May 06, 2024
Prefer strict types in Typescript

Types should be as close to reality as possible. Otherwise, they mislead (reduce truthiness in the system) and, thus, slow down development.

Usually, it's self-evident in many statically typed languages, but it's a different case with Typescript since it allows more types flexibility.

any

any may be a favorite type for many developers who migrated from Javascript recently. It declares that a type could be anything. There are some use cases for using any, but usually it's recommended to not employ it. One such example I could think of is silencing the Typescript compiler to run a script quickly. Otherwise, it detracts truth from a system and eliminates the benefits of using Typescript.

any type in typescript visualized

If you don't know what your data looks like, how are you going to handle it properly? Without proper handling, a system (or part of it) fails.

The other disadvantage of using any is silencing any potential errors you make when handling such data, which leads to another system failure.

const anything: any = 'word';
const result = anything * 5;

If you remove any declaration from the anything constant, Typescript tells you what is wrong exactly.

What if I don't know the exact type of data? Imagine I retrieve it from a third-party API that may change tomorrow. Or, from a non-typed npm package because it's written in Javascript. This is where we use unknown.

unknown

The useful unknown type states that a variable shape is not known and can be anything, the same point as any declares. The only difference is that unknown requires the compiler to check a type first before operating on data.

const anything: unknown = 5;
const result = typeof anything === 'number' ?
    anything * 5 :
    undefined;

The primary use case for unknown is to mark variables that are unknown and can be anything, so you must validate a type first before manipulating data.

unknown type visualized

If data shape is known, it's always better to specify an accurate type.

Narrowing

Narrowing is a process of giving types more accurate shapes. I.e., bringing them to reality, so they reflect a real data form.

To illustrate the motivation behind it, take a look at this code:

const record: Record<string, number> = {
    field: 2,
};
const result = record.field2 * record.field3; // NaN

Yes, record, is Record<string, number>, but it's not accurate enough. Record means an object with who knows how many fields and their values are of type number. In this case, a more accurate type is { field: number; }. In this example, you can skip declaring a type because the compiler can infer it automatically.

Another example of bad narrowing:

const fn: Function = (arg: number) => {
    return arg * arg;
};

fn('string'); // NaN

Yes, fn is a function, but this type doesn't specify the details (and as in the case above, you should skip declaring a type explicitly because the compiler will infer it for you automatically).

Common types to narrow

For these examples, T can be any type.

  1. Record<string, T>: prefer an accurate object shape, if you know it. E.g.,
type MyObject = { name: string; }
  1. T | null | undefined means you need to handle both null and undefined cases besides the presence of value. Prefer T | null, T | undefined, or merely T.
  2. Function: prefer an exact function signature, e.g.:
type MyFunction = (arg1: number, arg2: string): string;
  1. Partial<T>: prefer a more accurate type.

Originally published on absolyd.com

Discover and read more posts from Serhii C.
get started
post commentsBe the first to share your opinion
Show more replies