TypeScript is a language that extends the capabilities of JavaScript by adding type annotations to JavaScript code. This lets us avoid bugs from unexpected data types.
In this article, we’ll look at some antipatterns to avoid when writing TypeScript code.
Overusing the any Type
The point of using TypeScript is to have types in variables and functions. So we should use them wherever we can.
Therefore, we shouldn’t use the any
type in most of our code.
Overusing Classes
If our TypeScript classes don’t have many methods, then we don’t need to define a class just to use it to type variables and functions.
Also, if we only have a single instance, then wrapping the logic within a class doesn’t make sense.
Instantiating classes introduce complex and it’s hard to optimize when minifying code.
Instead, we can define object literals and use interfaces or the typeof
operator to get the type of an object.
For example, write the following interface for objects:
interface Person {
firstName: string;
lastName: string;
}
const person: Person = {
firstName: 'Jane',
lastName: 'Smith'
}
Then we can use them like we have above with person
.
Also, if we don’t know the exact structure that the object will have, we can use typeof
operator as follows:
const person = {
firstName: 'Jane',
lastName: 'Smith'
}
const person2: typeof person = {
firstName: 'Joe',
lastName: 'Smith'
}
Using typeof
is a convenient way to define types without writing lots of code.
If our object has methods, we can do the same thing as above:
interface Person {
firstName: string;
lastName: string;
fullName: (firstName: string, lastName: string) => string
}
const person: Person = {
firstName: 'Jane',
lastName: 'Smith',
fullName(firstName: string, lastName: string) {
return `${firstName} ${lastName}`;
}
}
TypeScript will do type inference with typeof
:
const person = {
firstName: 'Jane',
lastName: 'Smith',
fullName(firstName: string, lastName: string) {
return `${firstName} ${lastName}`;
}
}
const person2: typeof person = {
firstName: 'Joe',
lastName: 'Smith',
fullName(firstName, lastName) {
return `${firstName} ${lastName}`;
}
}
It’ll force us to add the fullName
method to person2
if it’s missing, and it’ll force us to add the parameters and return a string.
Otherwise, we’ll get compiler errors.
Using the Function Type
The Function
type is a generic type for functions. It’s like any
for variables. We should specify the data types of parameters and return types in our functions.
Instead, we should add types to parameters and return types as follows:
type ArithmeticFn = (a: number, b: number) => number
const add: ArithmeticFn = (a: number, b: number): number => a + b;
We have the types in the type alias ArithmeticFn
and also add
. That’s enough type annotations in our code.
Also, once again, we can use typeof
to do type inference:
const add = (a: number, b: number): number => a + b;
const subtract: typeof add = (a, b) => a-b
Then subtract
also has the same parameter and return types as add
.
Messing with Type Inference
We shouldn’t put useless type annotations with type inference will do the job.
For example, in the following example:
const courses = [{
name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse: any = {...course};
newCourse.description = 'Great intro to TypeScript';
courses[0] = newCourse;
We have an extra any
annotation that we shouldn’t have.
Instead, if we want to add a new property to an object after copying it, we can write the following:
const courses = [{
name: 'Intro to TypeScript'
}]
const [course] = courses;
const newCourse = {...course, description: 'Great intro to TypeScript'};
courses[0] = newCourse;
Then the TypeScript compiler won’t throw an error and we still get type inference from the TypeScript compiler since we didn’t use the any
type.
Copying and Pasting Partial Type Definitions
Another thing that we shouldn’t do is copy and pasting partial type definitions from other places into our own code.
If we want to get the type of an object with an unknown type, we can use the typeof
operator.
For example, we can write:
const person = {
firstName: 'Joe',
lastName: 'Smith',
age: 20
}
const person2: typeof person = {
firstName: 'Jane',
lastName: 'Smith',
age: 20
}
The TypeScript compiler will automatically recognize the type of person
and will do checks when we define person2
to check if everything’s there.
Lookup Type
We can also use one property of an object as its own type. This is called a lookup type.
For example, we can write:
const person = {
firstName: 'Joe',
lastName: 'Smith',
age: 20
}
const foo: typeof person.firstName = 'foo';
In the code above, TypeScript recognized person.firstName
‘s type is a string, so typeof person.firstName
would be a string.
Mapped Types
We can create mapped types to map all properties of a type to something else.
For example, we can make all the properties of a type optional by writing:
interface Person{
firstName: string;
lastName: string;
}
type Optional<T> = {
[P in keyof T]?: T[P];
};
const partialPerson: Optional<Person> = {};
The code above compiles and runs because we created a new type from Person
where all the properties are optional.
Getting the Return Type of a Function
We can get the return type function with the ReturnType
generic to pass in a type of the function. Then we’ll get the return type of that function.
For example, if we have:
const add = (a: number, b: number) => a + b;
const num: ReturnType<typeof add> = 1;
Then ReturnType<typeof add>
will be number
, so we have to assign a number to it.
TypeScript’s type inference is also working here. The ReturnType
generic is available since TypeScript 2.8
Conclusion
In many cases, we can use the typeof
operator, lookup types, or mapped types to annotate the types flexibly without losing type check capabilities.
This means we should eliminate the use of any
as much as possible.
Also, we don’t need classes just for adding types to a few objects.