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
- #1 Overusing the
any
Type - #2 Overusing Classes
- #3 Using the
Function
Type - #4 Messing up with Type Inference
- #5 Copy-Pasting partial Type Definitions
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.