· programming · 3 min read
Prefer strict types in Typescript
Strict types reflect reality and reduce a number of potential bugs in runtime
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.
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.
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.
Record<string, T>
: prefer an accurate object shape, if you know it. E.g.,
type MyObject = { name: string };
T | null | undefined
means you need to handle bothnull
andundefined
cases besides the presence of value. PreferT | null
,T | undefined
, or merelyT
.Function
: prefer an exact function signature, e.g.:
type MyFunction = (arg1: number, arg2: string): string;
Partial<T>
: prefer a more accurate type.