Telerik blogs
JavaScriptT2 Light_1200x303

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. ๐Ÿ˜‰

1. Use the Unknown Type Before Defaulting to Any

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.

2. The Never Type Can Be Handy for Error Handling

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;
}

3. Interfaces vs. Types

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.


4. Learn To Use Generic Types

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];
};

5. The Partial Utility Type Could Be Your New Best Friend

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. ๐Ÿ˜ƒ

6. Other Utility Types You Should Know

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.

7. Make Use of Type Guards To Access a Property Safely

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";
}
  • User-defined type guards.
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";
}

8. Custom Decorators Can Make Your Code Easier To Read (the Timing Function Example)

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 or jsconfig 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.


9. Use strictNullChecks and noUncheckedIndexAccess

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

10. Use noImplicitOverride (Especially for Mock Functions)

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.

Wrap-up

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.


author-photo_nada-rifki
About the Author

Nada Rifki

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 @RifkiNadaor come on her website, nadarifki.com.

Related Posts