TypeScript offers everything JavaScript does, plus static typing. Besides TypeScript’s type system, what made me fall in love with it is that it documents your code. Check out these 10 tips that will help you fall in love too!
In a nutshell, TypeScript is a programming language that offers all JavaScript features but with static typing enabled whenever you want to have it. The code is compiled to plain JavaScript, and the language—maintained by Microsoft—is gaining popularity every year as more and more popular frameworks are relying on it (Vue 3, AdonisJS, NestJS … ).
But, besides TypeScript’s type system, what made me fall in love with this language is that it documents your code. You can see in a glimpse what type a variable, a function’s argument or its response must be. This makes the developer experience so much nicer. π§βοΈ
This article is written for people who are already familiar with TypeScript. I want to share 10 quick tips I have learned along my developer journey. π
We all know that using the any
keyword is somehow evil. π
Fortunately, TypeScript 3.0 has introduced a new keyword called unknown
.
The main difference between any
and unknown
is that the unknown
type is only assignable to the any
type and the unknown
type itself. Thus, it is a much less permissive type and an excellent substitute for any
, as it will constantly remind your editor that you must replace it with something more explicit. By switching from any
to unknown
, we permit (almost) nothing instead of allowing everything.
Here is a quick example to illustrate what I mean:
let unknownValue: unknown;
let anyValue: any;
let unknownValue2: unknown = unknownValue; // This is fine
let anyValue2: any = unknownValue; // This is fine
let booleanValue: boolean = unknownValue; // Type 'unknown' is not assignable to type 'boolean'.
let booleanValue2: boolean = anyValue; // While this works
let numberValue: number = unknownValue; // Type 'unknown' is not assignable to type 'number'.
let numberValue2: number = anyValue; // While this works
let stringValue: string = unknownValue; // Type 'unknown' is not assignable to type 'string'.
let stringValue2: string = anyValue; // While this works
let objectValue: object = unknownValue; // Type 'unknown' is not assignable to type 'object'.
let objectValue2: object = anyValue; // While this works
let arrayValue: any[] = unknownValue; // Type 'unknown' is not assignable to type 'any[]'.
let arrayValue2: any[] = anyValue; // While this works
let functionValue: Function = unknownValue; // Type 'unknown' is not assignable to type 'Function'.
let functionValue2: Function = anyValue; // While this works
Another thing I like to do in my projects is to turn on noImplicitAny
. This flag will tell TypeScript to issue an error whenever something has the type any
(set by you or inferred by TypeScript). More details in the official documentation.
There is also another type we are never using when we start playing with TypeScript: the never
keyword. In a nutshell, it represents the type of values that never occur.
A while back, I found that this keyword can be handy when writing functions that trigger one or multiple errors and never return anything.
function throwErrors(statusCode: number): never {
if (statusCode >= 400 && statusCode <= 499) {
throw Error("Request Error");
}
throw Error("Something wrong happened.");
}
We can also use this type to our advantage by ensuring every critical situation is handled in our functions. Here is a quick example of when we forgot to test the case for the man species.
interface Dwarf {
weapon: "axe";
}
interface Elf {
weapon: "bow";
}
interface Man {
weapon: "sword";
}
type Specy = Dwarf | Elf | Man;
function whatIsThatGandalf(specy: Specy) {
let _ensureAllCasesAreHandled: never;
if (specy.weapon === "axe") {
return "This is a dwarf";
} else if (specy.weapon === "bow") {
return "This is an elf";
}
// ERROR: Type 'Man' is not assignable to type 'never'
_ensureAllCasesAreHandled = specy;
}
We commonly hear this question from people who just started their TypeScript journey: What is the difference between an interface and a type? Should I use both? π€
It took me a few weeks, in the beginning, to set a rule for myself as we can often do the same thing with both of them. Here is how I would sum it up: When I work with classes or objects, I use an interface. When I am not, I use a type. It would help if you remembered that the most important thing is to be consistent with your choices inside your codebase.
Of course, there are also some subtle differences between both, as explained in this great video. For instance, an interface can extend other interfaces, while you need a union or an intersection to merge two types together (as they are static).
Also, from my experience, when I deal with types, the error message can usually be more brutal to understand when something goes wrong.
Another thing to keep in mind is that the TypeScript documentation does encourage you to use an interface when possible, especially if you are writing a library that exports a type. The reason is that an interface can be extended to fit the need of the application that is using your code.
Generic types are handy. The more you get familiar with TypeScript, the more you use them. They allow your code to be more flexible by allowing you to set the type yourself. An example will paint a thousand words. π
Let’s say that we want to freeze or turn all properties of an object into read-only properties. Well … we could do something like this.
interface Elf {
name: string;
weapon: "bow";
}
const myElf: Elf = {
name: "Legolas",
weapon: "bow",
};
const freezedElf = Object.freeze(myElf);
// ERROR: Cannot assign to 'name' because it is a read-only property.
freezedElf.name = "Galadriel";
Now, let’s use a generic type to do the same thing.
interface Elf {
name: string;
weapon: "bow";
}
const myElf: Elf = {
name: "Legolas",
weapon: "bow",
};
type Freeze<T> = {
readonly [P in keyof T]: T[P];
};
const freezedElf: Freeze<Elf> = myElf;
// ERROR: Cannot assign to 'name' because it is a read-only property.
freezedElf.name = "Galadriel";
// For your information, the generic type Freeze already exists in TypeScript and is called Readonly.
// https://www.typescriptlang.org/docs/handbook/utility-types.html
// So both are equivalent
const freezedElf: Freeze<Elf> = myElf;
const freezedElf: Readonly<Elf> = myElf;
If you are wondering, the
keyof
operator takes an object type and produces a string or literal numeric union of list keys.
As you can see in the example above, what is remarkable is that no matter the object’s shape, we can create another type (Freeze<Elf>
) that will include all the properties set to read-only.
Here is another generic type that will set all properties of the object as non-read-only.
type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
One last example here is a function using a generic type that returns the last element in an array.
const getLastElement = <T>(array: T[]) => {
return array[array.length - 1];
};
What if we would like to create a new type based on another type but with all its properties set to optional. How could we do this? π€
TypeScript ships with a utility I use every week called Partial<Type>
. Here is how it works.
interface Elf {
name: string;
weapon: "bow";
lifepoints: number;
}
type PartialElf = Partial<Elf>;
// We can omit the weapon attribute as all properties are now optional
const partialElf: PartialElf = {
name: "Legolas",
lifepoints: 100,
};
// This is how it is coded behind the curtain
type Partial<T> = {
[P in keyof T]?: T[P];
};
Great, right? Now, let’s dive into other utility types that you will love to use in your project. π
You have learned about the Partial<Type>
to construct a type with all properties of Type
set to optional. But there are more of them. Here are the ones I often use:
Required<Type>
: Constructs a type consisting of all properties of Type
set to required. As you can guess, this is the opposite of Partial.interface Properties {
a?: number;
b?: string;
}
const object: Properties = { a: 5 };
// ERROR: Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Properties>'.
const object2: Required<Properties> = { a: 5 };
Readonly<Type>
: Constructs a type with all properties of Type
set to read-only, meaning the properties of the constructed type cannot be reassigned.interface Todo {
title: string;
}
const todo: Readonly<Todo> = {
title: "Learn Kendo UI",
};
// ERROR: Cannot assign to 'title' because it is a read-only property.
todo.title = "Hello";
Pick<Type, Keys>
: Constructs a type by picking the set of properties Keys
(string literal or union of string literals) from Type
.interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Learn DevCraft",
completed: false,
};
Omit<Type, Keys>
: Constructs a type by picking all properties from Type
and then removing Keys
(string literal or union of string literals).interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit<Todo, "description">;
const todo: TodoPreview = {
title: "Learn Kendo UI",
completed: true,
createdAt: 1615277055442,
};
NonNullable<Type>
: Constructs a type by excluding null
and undefined
from Type
.// T will be equivalent to string | number
type T = NonNullable<string | number | undefined | null>;
To browse the complete list of utility types available globally, head over to the official documentation.
Bulletproof code makes use of type guards a lot. TypeScript makes it easier to know when we should use one. π
To sum it up, type guards allow you to check if an object belongs to the right type. It is a protection we use inside our code to make sure nothing wrong occurs. What is excellent with TypeScript is that, with the errors displayed right in the editor, we can guess when to add type guards.
Here are some standard type guards you can use.
typeof
: The typeof
operator returns a string indicating the type of the unevaluated operand.function stringOrNumber(x: number | string) {
if (typeof x === "string") {
return "I am a string";
}
return "I am a number";
}
instanceof
: The instanceof
operator tests to see if the prototype property of a constructor appears anywhere in the prototype chain of an object. The return value is a boolean value.class Dwarf {
weapon = "axe";
}
class Elf {
weapon = "bow";
}
function dwarfOrElf(specy: Dwarf | Elf) {
if (specy instanceof Dwarf) {
return "I am a dwarf";
}
return "I am an elf";
}
in
: The in
operator returns true if the specified property is in the specified object or its prototype chain.interface Dwarf {
weapon: "axe";
lifepoints: number;
}
interface Elf {
weapon: "bow";
}
function dwarfOrElf(specy: Dwarf | Elf) {
if ("lifepoints" in specy) {
return "I am a dwarf";
}
return "I am an elf";
}
interface Dwarf {
weapon: "axe";
lifepoints: number;
}
interface Elf {
weapon: "bow";
}
function isDwarf(specy: any): specy is Dwarf {
return specy.lifepoints !== undefined;
}
function dwarfOrElf(specy: Dwarf | Elf) {
if (isDwarf(specy)) {
return "I am a dwarf";
}
return "I am an elf";
}
I love when I use a framework or a library that is making good use of decorators. While some people believe that they are an antipattern and should not be used, the reality is a little more complex. They can often make the code easier to read and faster to write. Frameworks like AdonisJS, NestJS or even Angular make use of decorators a lot.
Decorators can be applied to class definitions, properties, methods, accessors and parameters. They represent functions that will alter the behavior of your code.
They are easy to write. Here is how we can create a decorator that will compute how long a function takes to run.
function time(name: string) {
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
const fn = descriptor.value;
descriptor.value = (...args) => {
console.time(name);
const v = fn(...args);
console.timeEnd(name);
return v;
};
};
}
class Specy {
@time("attack")
attack() {
// ...
}
}
You may have a warning inside your editor as experimental decorator support is a feature that is subject to change in a future release. Set the “experimentalDecorators” option in your
tsconfig
orjsconfig
to remove this warning.
If you want to learn more about decorators, you should check this awesome video from Fireship (which is also an excellent coding channel you should subscribe to π).
One word of caution: Inherited classes will receive the functionality of the decorator.
There are two flags I usually turn on inside my TypeScript configuration file: strictNullChecks
and noUncheckedIndexAccess
.
strictNullChecks
: By default, null
and undefined
are assignable to all types in TypeScript. With this flag turned on, they will not be.let foo = undefined;
foo = null; // Will trigger an error
let foo2: number = 123;
foo2 = null; // Will trigger an error
foo2 = undefined; // Will trigger an error
noUncheckedIndexAccess
: TypeScript has a feature called index signatures. These signatures are a way to signal to the type system that users can access arbitrarily named properties. With this flag turned on, they won’t be able to.const nums = [0, 1, 2];
const example: number = nums[4]; // WIll trigger an error
This is probably the most useful tip from this list for people who write a lot of tests.
I will explain myself. When we create mocking functions and classes to check that our code is behaving correctly, we do not want people to rename a function inside this initial class without being notified that they should also change it inside the mocking class. Well … TypeScript will help you with this.
The first thing to fix this issue is to turn on noImplicitOverride
in your TypeScript configuration file and use the override
keyword.
class Specy {
lifepoints = 10;
heal(lifepoints: number) {
this.lifepoints += lifepoints || 10;
}
}
class MockSpecy extends Specy {
override heal(lifepoints) {
console.log("Let's override the heal method");
}
}
If someone changes the heal
function into healing
inside the Specy
class, TypeScript will display an error inside the MockSpecy
class to tell us that we should also update the method here.
You did it! These were ten quick tips about TypeScript I wanted to share with you. π
One last piece of advice is that if you want to sharpen your knowledge about this beautiful language, you should choose a framework based on it and dive into its codebase—something like Nest, Adonis or Vue 3. The more you read TS code, the more you will discover great things you can do with it.
I am also happy to read your comments and your Twitter messages @RifkiNada. And in case you are curious about my work, you can have a look at it here www.nadarifki.com. π
P.S. You can learn how to debug your TypeScript code with Visual Studio Code. Here is a quick video to do so.
Nada is a JavaScript developer who likes to play with UI components to create interfaces with great UX. She specializes in Vue/Nuxt, and loves sharing anything and everything that could help her fellow frontend web developers. Nada also dabbles in digital marketing, dance and Chinese. You can reach her on Twitter @RifkiNada or visit her website, nadarifki.com.