3 TypeScript patterns that I use in my day-to-day work

On readonly properties, type guards and unions

Ivan Spoljaric
Dev Genius

--

Photo by Aryan Dhiman on Unsplash

If you would like to see more JS articles like this one, or if you’re interested in my services, take a look at my blogfolio: https://js-blog.dev/.

1. Readonly properties

In TypeScript 2.0, the readonly modifier was added to the language.

Properties marked with readonly can only be assigned during initialization or from within a constructor of the same class. All other assignments are disallowed.

  • Example
// type definitions

interface Person { readonly name: string; readonly age: number; };

// or

type Person = { readonly name: string; readonly age: number; };

// Usage:

const person: Person = { name: Mike, age: 23 };

Now, because properties name and age are declared as readonly, the value of either cannot be reassigned at compile time:

person.name = "Kate" // not allowed
person.age = 33 // not allowed

This gives the developer an additional layer of security because the compiler checks for unintended property assignments during development. And this reduces the number of potential runtime bugs.

It is also worth explicitly stating that the readonly modifier is a part of TypeScript’s type system, and that once the Typescript code has been compiled to JavaScript, the readonly modifiers are stripped from the code. So there is no runtime protection against property reassignments at runtime.

2. Type guards

Type guarding is a technique used to get the type of a variable, within a conditional block.

Type guards are typically implemented as functions.

Type guards allow developers to detect the correct methods, prototypes, and properties of a value.

TypeScript uses some built-in JavaScript operators like typeof, instanceof, and the in operator, which are used to determine if an object contains a property.

  • Example 1 — using the “typeof” operator to construct a type guard
const isString = (value: unknown): value is string => typeof value === "string";

// ....

if (isString(value)) {
console.log(value.toUpperCase());
}

The typeof operator can determine the following types recognised by JavaScript; boolean, string, bigint, symbol, undefined, function, number

  • Example 2 — using the “in” operator to construct a type guard
interface Vehicle {
manufacturer: VehicleManufacturer;
model: VehicleModel;
}

interface Animal {
name: string;
species: AnimalSpecies;
}

const getUniqueID = (entity: Vehicle | Animal) => {
if("manufacturer" in entity) {
return entity.manufacturer;
}

if("species" in entity) {
return `${entity.name} - ${entity.species}`;
}

...
}

The in operator checks if an object has a particular property. It is also usually implemented as a function and it returns a boolean.

It is very useful for narrowing features like in the example above.

3. Union types

A union type describes a value that can be one of several types. We use the vertical bar (|) to separate each type, so number | string | boolean is the type of a value that can be a number, a string, or a boolean.

  • Example 1 — a union of primitives
type Status = "active" | "inactive" | "pending";

const setStatus = (status: Status) => {
// ...
}

setStatus("active");
setStatus("pending");
setStatus("invalid"); // Error: Argument of type '"invalid"' is not assignable to parameter of type 'Status'.
  • Example 2 — narrowing the union

Building on the previous section regarding type guards, the example below demonstrates how these two TS concepts work together.

// type guards

const isString = (value: unknown): value is string => {
return typeof value === "string";
}

const isNumber = (value: unknown): value is number => {
return typeof value === "number";
}

// simple union type

type ID = "string" | "number";

const getNameFromID = (id: ID) => {
if(isString(id)) {
// logic that deals with id: string
}

if(isNumber(id)) {
// logic that deals with id: number
}

// default return, or error handling logic
}

getNameFromID("23");

getNameFromID(45);
  • Example 3 — union of object types
interface Bird {
weight: number;
canFly: boolean;
}

interface Dog {
age: number;
canFly: boolean;
}

const canAnimalFly = (animal: Bird | Dog) => animal.canFly;

// even better for readability and scalability purposes

type Animal = Bird | Dog;

const canAnimalFly = (animal: Animal) => animal.canFly;

This example uses a union of object types instead of primitives.

Written as is, there are no TS compiler problems.

But what if we wanted to access the weight property on a bird: Bird object, or the age property on a dog: Dog object?

type Animal = Bird | Dog;

const bird: Animal = bird.age // TS error

The TypeScript compiler would complain and throw an error.

Because TS is a structurally typed language, it allows access only to properties defined on both interfaces.

So here is a modified example of the above code using the type guard technique that helps us to satisfy the TS compiler:

interface Bird {
weight: number;
canFly: boolean;
}

interface Dog {
age: number;
canFly: boolean;
}

type Animal = Bird | Dog;

const getAnimalAge = (animal: Animal) => {
if("age" in animal) {
return animal.age;
}

return undefined;
}

const getAnimalWeight = (animal: Animal) => {
if("weight" in animal) {
return animal.weight;
}

return undefined;
}

Now imagine we added more shared properties to either Bird or Dog interface. Or what if we removed some of the shared properties from both interfaces that are also used inside the type guards? Like canFly.

A more “bulletproof” implementation, which would work for more complicated types with a lot more overlap and depth, would be to attach an additional identifier property to both interfaces that we could also use to identify the parameter type at runtime.

Like this:

interface Bird {
weight: number;
canFly: boolean;
ANIMAL_TYPE: "BIRD";
}

interface Dog {
age: number;
canFly: boolean;
ANIMAL_TYPE: "DOG";
}

type Animal = Bird | Dog;

const getAnimalAge = (animal: Animal) => {
if(animal.ANIMAL_TYPE === "DOG") {
return animal.age;
}

return undefined;
}

const getAnimalWeight = (animal: Animal) => {
if(animal.ANIMAL_TYPE === "BIRD") {
return animal.weight;
}

return undefined;
}

A downside to this approach is that we are attaching an additional property to our JS objects (which have no business value). And this increases memory consumption - but probably not by much.

Conclusion

Thank you for reading this article until the end.

I hope you learned something new today. And if not, maybe this helped you solidify some concepts.

I will be publishing more of these in the future, so stay tuned :)

Resources:
1)
https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html

2) https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html

--

--

Software Developer | Freelancer @ Toptal | Remote Worker | Lifelong Learner