TypeScript anti-patterns

December 30, 2018 • 11 min read

The purpose of this article is to avoid common mistakes while using TypeScript. I've been using TS in several big-to-very-big commercial projects since early 2015. Throughout that time I've encountered some misconceptions and mistakes about declaring types, as well as I was asked questions during my TypeScript trainings. Some of them reappear pretty often, so I made my best to extract the common roots of the misconceptions and explained them below.

Overview

TypeScript is all about restricting the types of expressions and variables we use. It's about reducing what we can do with our own (or extenal) code. So expect that all the following topics will relate to making our type definitions more strict :).

#1 Overusing the any Type

Yes, this has to be point no. 1 since it's the most often. Unfortunately, it happens too often that developers believe the promise of better code quality with static typing, but they don't will to spend this little bit more time on precisely defining the types they use. And it needs repeating over and over again: you shall (almost) never use any.

#2 Overusing Classes

Many developers who come from class-based languages such as Java, C#, PHP, etc. feel a relief when finally finding out that we can have (statically) typed JavaScript. That's refreshing and I felt the same some time ago :) And - as long as TypeScript allows to write both Object-Oriented (OOP) and Functional Programming (FP) - many of us take OOP as the default choice and keep on introducing classes throughout the TypeScript code. If we need to have multiple instances - that's expected. But if we need only a single instance - wrapping the logic with a class is just not needed.

Keep in mind I'm not favouring one over the other (OOP vs FP), each has its strengths and weaknesses.

When using TypeScript, we should consider two concepts:

instantiating a class introduces complexity

Before we can use the logic of a class, first we have to call the constructor to get the instance. We need to decide who creates the instance (responsibility, ownership) and when to do it. Although it's not a huge complexity (are there any means to measure it?), still, it's some complexity which doesn't have to be there (see accidental complexity). The essential complexity lies in the methods themselves. Also, passing arguments through a class constructor is not always needed, e.g. if the value is available via import statement.

Instead, if we need only a single instance, we can define an object literal, thereby removing the need for calling the constructor. We can have the instance straight away. Using object literals is idiomatic JavaScript. And TypeScript is just JavaScript + static typing.

type inference works very well for object literals

Type safety for classes is great:

class MyValidator {
    validateEmail(email: string): boolean { 
        return true; // :P
    }
}

const validator = new MyValidator()
validator.validateEmail() // throws

but type safety for object literals - thanks to type inference - is just as well. We don't lose anything:

const myValidator = {
    validateEmail(email: string): boolean { 
        return true; // :P
    }
}

validator.validateEmail() // throws

Basically, let's not overcomplicate our code. You might not need a class, if an object literal - or even a set of functions (if the object was meant to be stateless) - would do the job.

#3 Using the Function Type

This one is important whenever our APIs accept callbacks and we want to restrict that certain parameters of functions/methods are themselves functions. So what should be the type of the callback function?

Although TypeScript allows to use the Function type, it's very rarely a good idea, since it's very rare just any function. And Function is like the any type applied to functions. It tells us only that an expression of this type is invokable/callable (we can invoke/call the function with given arguments). But most often we do know what parameters such function supports and we can improve type safety. With Function we lose not only the input parameters types, but also the result type.

Instead of:

type aThing = {
  doSomeLogic(cb: Function)
}

we should define function types, such as:

type ArithmeticFn = (a: number, b: number) => number
type aThing = {
  doSomeLogic(cb: ArithmeticFn)
}

#4 Messing up with Type Inference

Let's take a snippet from this angular video tutorial. This small snippet is basically about creating an immutable copy of a course object with a modified description field:

// original item to be changed in an immutable manner
const course = this.courses[0];

const newCourse: any = {...course};
newCourse.description = 'New Value!';
this.courses[0] = newCourse;

Whether we want the code to be a one-liner or not (preferences, conventions, whatever...) we should use idiomatic JavaScript and let the type inference do everything for us:

const course = this.courses[0];
const newCourse = {...course, description: 'New Value!'};
this.courses[0] = newCourse;

In this case, the object destructuring with overriding a certain field is really enough for TypeScript to infer the newCourse variable precisely. And to find out that newCourse is type compatible with course both ways :) The general rule of thumb is that sometimes removing any will allow type inference to do all the job (occurence of any is redundant and actually harmful).

Just want to emphasize, that Angular University (who created above video) provides really great content and I don't mean to criticize it, just nobody is perfect and this mistake is worth analyzing.

#5 Copy-Pasting partial Type Definitions

There are lots of neat tricks we can use in order to avoid creating types manually or copy-pasting parts of types with or without applying changes. Here are some of them.

Extracting the type of an existing Literal Object

Example usecase: we're diving into a big legacy JS application. We find that there's a global config object defined like this one:

var Configuration = {
  API: "http://host/path/to/api",
  token: "jw3t-4w4j-5t04-5jt0-445t-fe98",
  locale: "en-us",
  language: "en",
  currency: "USD",
  modules: ["admin", "orders", "stock"]
}

And it's passed as an argument to many functions throughout the codebase. What type should we provide for this config?

We can define the whole type from scratch, but we don't have to. Simply, use typeof:

type AppConfig = typeof Configuration

to get the type (type AppConfig = { API: string; token: string; locale: string; lang: string; currency: string; modules: string[]; })

example entity

For the rest of examples, let's use a pretty big Employee entity from my IT Corpo mock API which I use during my trainings:

type Employee = {
  "id": number;
  "nationality": Nationality,
  "departmentId": number;
  "keycardId": string;
  "account": string;
  "salary": Money;
  "office": [string, string];
  "firstName": string;
  "lastName": string;
  "title": string;
  "contractType": ContractType;
  "email": Email;
  "hiredAt": DateString;
  "expiresAt": DateString;
  "personalInfo": {
    "age": number;
    "phone": Phone;
    "email": Email;
    "dateOfBirth": DateString;
    "address": {
      "street": string;
      "city": string;
      "country": string;
    };
  },
  "skills": Skill[];
  "bio": string;
};

Lookup Types

Example usecase: need to fetch employees that are assigned to a certain department ("departmentId": number).

Although the following should work:

const getEmployeesByDepartmentId = (departmentId: number): Response { ... }

it's not a good idea to do it, because it will work now. That's not easy to spot, but in the code above we're losing the single source of truth about the Employee entity shape, when introducing a loose number.

The departmentId should be a derivative of the Employee entity, in TypeScript we call it a lookup type (we could optionally create a separate typedef for this field's type):

const getEmployeesByDepartmentId = (departmentId: Employee["departmentId"]): Response { ... }

Thanks to it, whenever the Employee entity gets updated, all places that depend on its derivatives get updated and potentially our components/redux/ngrx/whatever code will throw errors, since number is now expected to be a string (guid). If we leave just departmentId: number, we get a silent fail. o_O

That's a good strategy especially for long-living, big applications that are likely to evolve over time. Just keep the single source of truth.

Mapped Types and Conditional Types

Need to use the type, but in a slightly modified version?

Example usecase: let's say, our app has to support HTTP PATCH requests, that is, modify an existing resource, but instead of replacing existing one with the whole new value (HTTP PUT), we pass only the part that changed. For instance, our interface supports changing the firstName and lastName values via input in a profile form, changing the contractType (enumeration) via a dropdown in a contracts form and skills list in a résumé form. Oh, and changing personalInfo is possible via another form. All changes should be possible independently.

Using HTTP PUT we'd have to load existing entity, apply the changes and resend the whole object. In some situations this loading + applying is not necessary, thus HTTP PATCH, which sends just the diff. What we're aiming at is providing a type that will allow to define any part of the entity - and will be used to update only the pieces that our form supports.

In TypeScript that's super simple:

type PartialFieldsEmployee = Partial<Employee>;

Partial is an example of so called Mapped Types. The idea behind these is to basically map over all fields of an object's type apply a chang and return the new type. So it works similarly to Array.map in JavaScript. The internals of Partial, which BTW works as well, are:

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

TypeScript is just applying the ? to make all fields optional on the top level. You can do the opposite (make all fields required) by applying Required:

type RequiredFieldsEmployee = Required<PartialFieldsEmployee>;

it's internals are analogical:

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

Accessing Internal types of an External Library

I've had experience with devextreme. First thing that comes to my mind about this library is that its support for TypeScript could be way better. Bottom-line for us, developers: sometimes you encounter a great, full-featured JS library which has pretty limited TS support and gotta deal with it.

I'm using the github snapshot (browsing code from the latest commit at the time of writing this article, December 30, 2018).

Example usecase: you need to access the return type of a grid method

This is the line of our interest:

export class dxDataGrid extends GridBase {
  ...
  getSelectedRowKeys(): Array<any> & Promise<any> & JQueryPromise<any>;
}

Same as above, we don't want to copy-paste it, since it would desync us from the original type definition (new releases of the library that introduce any change to the type would make our code break). Instead of copying the datatype, let's access it with ReturnType:

type GridSelectedRowKeys = ReturnType<dxDataGrid["getSelectedRowKeys"]>

As you can see, above is a combination of the ReturnType conditional type and a type lookup. This works since dxDataGrid is a type definition. If we've had an implementation (instead of an interface), like the following:

function myFn(): ABC {
  return someData;
}

then ReturnType<myFn> would not work, because we're mixing the type context with implementation context. We need to dump the implementation of myFn down to it's type with typeof myFn and access it further with ReturnType<...> same as above. That makes: ReturnType<typeof myFn>. It works correctly, since ReturnType expects the generic passed to be a type, and what it gets is indeed a type definition (not an implementation).

Another example usecase: we need to access a grid's method parameter type

In ideal world, it would just be exported as a type. But apparently, it's not. Funfact, the method accepts only 1 parameter, the event_, which has _lots of fields and we need to use this event somewhere else in our application. And no copy-pasting.

This is the line of our interest:

export class dxDataGrid extends GridBase {
  ...
  onCellHoverChanged?: ((e: {
    component?: dxDataGrid,
    element?: DevExpress.core.dxElement,
    model?: any,
    eventType?: string,
    data?: any,
    key?: any,
    value?: any,
    text?: string,
    displayValue?: any,
    columnIndex?: number,
    rowIndex?: number,
    column?: dxDataGridColumn,
    rowType?: string,
    cellElement?: DevExpress.core.dxElement,
    row?: dxDataGridRowObject
  }) => any);
}

Originally, that's a single line for the library's internal convention consistency, here broken down for readability. We need to access what is the type of e above. The following code does the job:

type FirstParameterType<T> = T extends (e: infer R) => any ? R : any;
type OnCellHoverChangedEvent = FirstParameterType<dxDataGrid["onCellHoverChanged"]>

It's probably one of the most complex pieces in this post, so it requires additional explanation. The FirstParameterType is a conditional type which allows us to intercept the function signature (note: not a function body/implementation!) and extract it's first parameter. The syntax basically requires us to put infer R in the spot we want to infer and ? R : any as the resulting type (R - the inferred one, if matched, otherwise any). So the FirstParameterType, given a function signature as its generic type, will return the type of the first parameter.

We access the method signature via dxDataGrid["onCellHoverChanged"]. So all in all, our solution is: FirstParameterType<dxDataGrid["onCellHoverChanged"]>.

Summary

Pay attention not to make your types to broad, especially, don't make them allow anything. And try to keep as many single sources of truth for your type definitions as possible - and anywhere you need to reuse it - make it a derivative type instead of a modified copied typedef.