Tomasz Swistak

|
|

Avoiding Any in TypeScript – Advanced Types and Their Usage

TypeScript gives JavaScript developers an opportunity to provide strict types in a code. However, due to the nature of JavaScript, in some cases providing accurate types isn’t a simple task. In such situations programmers are tempted to use any – a typing picklock which allows the user to store anything in it. In this article I’d like to show some built-in types and built-in TypeScript features which you can use to avoid any or to simplify some custom typings.

JAVASCRIPT OBJECTS

Probably, nearly every JavaScript developer at some time has used an object as a map-like collection. However, with strict types it may not be that obvious how to type this (especially with the no-string-literal TSLint rule). The first go-to option is any, but we are losing information of value type. So, we may strictly type an interface with certain keys – but this way we can’t add anything to the object. Another option is Object or {}, but this means “empty object,” so we can’t set anything in it. So, what’s the standard way?

Record

Let’s see the definition:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Listing 1 Record definition from lib.es5.ts

And the usage:

const dict: Record<string, number> = {};
dict.a = 1;
dict.b = 'a'; // ERROR: "a" is not assignable to type number

Listing 2 Example of using Record type

As you can see, it means that the developer can enter any key, but the value has to be of a specific type. What’s more, we can combine it with other types, therefore making dictionaries with some pre-defined keys (of even different value types!) visible by IntelliSense, e.g.:

const dict2: { a?: string } & Record<string, number> = {};
dict2.a = 'a';
dict2.a = 1; // ERROR: 1 is not assignable to type string
dict2.b = 2;
dict2.b = 'b'; // ERROR: "b" is not assignable to type number

Listing 3 More advanced example of using Record type

I don’t think I need to write about the practical usage of this. Of course, we have a built-in ES6 collection called “Map” that offers a much better experience for dictionaries, but according to benchmarks, when we only want to set and get values, it’s faster to use objects (Record).

MASS MODIFIERS OF OTHER TYPE’S PROPERTIES

What to do, when we already have a type with defined properties, but we don’t like how it is defined? Maybe we want all properties to be optional, required, or read only? TypeScript also has it covered with the types PartialRequired and Readonly. Let’s see their definitions:

type Partial<T> = {
    [P in keyof T]?: T[P];
};
type Required<T> = {
    [P in keyof T]-?: T[P];
};
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Listing 4 Partial, Required and Readonly definitions from lib.es5.ts

Of course, as the article’s title tells us, only Partial can really be used in place of any, but all three are in a way linked to each other, so it would be a waste to not to write about them together.

They offer some quite practical usages. Readonly is very common in typings of React – in components there are some things that you can’t just edit, but you are the person who defines types like a component’s state or properties.

Partial is used wherever you want to offer the possibility to not provide the whole object. A popular usage example is providing the possibility to set the values of an object through one function (or constructor), rather than by setting properties separately. The following listing gives an example of this kind of usage.

class Settings {
    setting1: boolean = true;
    setting2: string = '';
    setting3: number = 1;
    constructor(obj: Partial<Settings>) {
        Object.assign(this, obj);
    }
}
const obj = new Settings({ setting3: 2 });

Listing 5 Example of using Partial

Required is the opposite of Partial. However, here the usages are not so straightforward, since it’s checking if everything is set just at the types level during compilation. Since I’ve never seen any good, practical usages, I won’t provide any. If you have some, please let me know.

PICKING AND OMITTING SPECIFIC A PART OF A TYPE

Earlier we were discussing Partial, which allowed us to make everything in a type optional. That said, we may also want to take a specific part of an object without changing it to be optional (which makes typing even more strict). Let me introduce Pick:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Listing 6 Pick definition from lib.es5.ts

Pick works like this: it takes only those specific properties that are in the second type. Here’s an example of how it works:

type C = { a: string; b: string; c: boolean; d: string; }
type D = { b: string; c: boolean; }
type pickCkeys = Pick<C, 'c'>
type pickCD = Pick<C, keyof D>;
const c: pickCkeys = { c: true }; // a, b, d doesn't exist
const d: pickCD = { b: 'a', c: true }; // a, d doesn't exist

Listing 7 Usage of Pick

Pick may not seem very useful, since in fact it only takes the given keys or keys from the second type. So you might ask: why don’t we just use this second type or build a type only from those keys? The reason is this – we keep information about the original type. And why not Partial? The difference is when you turn on strictNullChecks flag in compiler settings. Let’s review an example:

type E = { a: string; b: string; }
function withPick<K extends keyof E>(state: Pick<E, K>): void { return; }
function withPartial<E>(state: Partial<E>) { return; }
withPick({ a: null }); // 'null' is not assignable to type string
withPartial({ a: null }); // no error

Listing 8 The difference between Pick and Partial

As you can see, Pick doesn’t allow setting a null value (also undefined) as was defined in the original type. Partial just makes everything nullable, so it does interrupt the original typing. In practice it’s used, for example, in React, in the setState function.

That said, I’ve mentioned omitting. We don’t have a built-in type for that, but we can build one for ourselves like this:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

Listing 9 Definition of Omit type

In this way, we can perform a following thing:

type C = { a: string; b: string; c: boolean; d: string; }
type D = { b: string; c: boolean; }
type ComitKeys = Omit<C, 'c'>;
type ComitD = Omit<C, keyof D>;
const e: ComitKeys = { a: 'b', b: 'b', d: 'd' }; // c doesn't exist
const f: ComitD = { a: 'b', d: 'd' }; // b, c doesn't exist

Listing 10 Example of using Omit

If you compare objects e and f from this listing with c and d from listing 7, you can see that we get the complete opposite thing. An example of using Omit can be seen in Ramda to describe the result of a function called omit (quite a surprise, huh?).

UNKNOWN AND ADVANCED TUPLE TYPES

Both TypeScript features which I describe briefly in this part of the article were introduced in TypeScript 3.0, and I’ve already covered them in my article about TypeScript 3.0.

unknown is what should be used when we don’t know a proper type of object. Unlike any, it doesn’t let you do any operations on a value until we know its type (through proper type checks).

Through advanced tuple types I mean mostly extracting and spreading function arguments with tuple types, which was introduced in TypeScript 3.0. Thanks to them, we can define specific types for specific values in an array. While at first glance it doesn’t sound very helpful, they can be useful in typing functional programming features like compose.

I encourage you to review the article I’ve linked above for more detailed descriptions, along with usage examples, so you can have a better grasp of them.

SUMMARY

TypeScript has a lot of typing weaponry ready to be used to keep static types in the dynamically typed environment of JavaScript. The whole problem is that it’s hidden and its usage is not always so straightforward. However, while this article hasn’t described everything that TypeScript offers, I hope it will help you in writing code which will be typed better.

This post was also published on ITNEXT.

Do you need more technical insights? Check out my other articles!