7 Ways to Up Your TypeScript Game

Written by danielleford04 | Published 2024/04/03
Tech Story Tags: typescript | front-end-development | frontend-development | front-end-developer | typescript-tips | typescript-guide | typescript-features | programming-guide

TLDRMaster TypeScript with essential features: unions, nullish coalescing, object typing, overload signatures & more!via the TL;DR App

TypeScript’s increasing popularity in recent years has made it something many front-end developers have started to learn. As a front-end engineer, I was regularly told it was pretty simple and would be easy to just pick up, given my background in JavaScript. When I finally started working somewhere where we used TS, while it was true some of the basics were pretty easy to pick up, a lot of the features that make TypeScript useful were not necessarily intuitive enough or used commonly enough to just pick up casually from working within a codebase.

Over the years, here are a few TypeScript features that I’ve learned that are really helpful in using TypeScript effectively.

  1. Discriminated unions - aka conditional sets of fields within types

    Sometimes, we have components that take in data that is very similar but not quite identical. In many cases, making a field optional works to resolve this. However, there are situations where either one or the other property is required, or if one property exists, a second property is required.

    For instance, if we have a button component that can either function as a link (which requires a path to navigate to) or a traditional button (which requires a function to pass to an onClick handler) - we don’t want to just make the path and onClickFunctions optional, because we need to have one or the other. Another example is an appointment object - some appointments might be single appointments, and some might be recurring. If it’s a recurring appointment, we need to include the recurrence interval.

Discriminated unions allow us to have alternate sets of type parameters. If there are no shared fields, the simplest way to use discriminated unions looks like this:

type RecurringAppointment={

  type: recurring

  recurrence_interval: 7  recurrence_unit: days}

type SingleAppointment={  type: single

}

type Appointment = RecurringAppointment | SingleAppointment;


However, in many cases, there are going to be shared fields (like the appointment name and date) that make it easier to combine everything in one type of declaration, like so:

type Appointment={

  name: string  date: string

}&(

{

  type: recurring

  recurrence_interval: 7  recurrence_unit: days

}|{

  type: single

});

This syntax doesn’t work with interfaces, which you may be using for their extension syntax (“extends”). In this use case, you use the following syntax:

Instead of:

interface Appointment extends AppointmentStylesProps

Do this:

type AppointmentProps = AppointmentStylesProps & {

   name: string    date: string

 } & (

     | {

   type: recurring

   recurrence_interval: 7    recurrence_unit: days

       }

     | {

   type: single

       }

   );

  1. Cleaner null checks - Nullish coalescing operator

Nullish coalescing operators are technically a JavaScript feature, but TypeScript added support for them with TS 3.7, and knowing how to use it can make certain type definitions a lot cleaner.

Null checks - or checking that a variable has a value - are a regular part of writing code. However, it’s not always as simple a task as it seems like it should be - when using the boolean OR operator ( || ), any falsey values (0, ‘’, etc) are treated as falsey rather than as set values.

let isUserActive = user.active || true // will always return true, even if we want it to return falselet divHeight = height || “Please enter a height”  //will not work if the height is 0

function convertToString(s: string | undefined): string {

 return s || ‘hello world’;

} //will change empty strings to ‘hello world’ even though they are already strings

One alternative is to specifically check for undefined and null individually. Another is to check the typeof your variable, and if the type is correct, preserve the value. If you only have to do this once, these solutions aren’t so bad, but given how often null checks are required, it adds up, increasing the complexity of code, making the code more difficult to read, and increasing the likelihood something gets missed and bugs are introduced.

let isUserActive = typeof user.active === “boolean” ? user.active : true;

let divHeight = typeof height === “number” || “Please enter a height”

function convertToString(s: string | undefined): string {

 return s === undefined ? ‘’ : ‘hello world’;

}


Luckily, there is a built-in solution for this. The nullish coalescing operator, ??, is essentially a specialized version of || that treats both null and undefined (and nothing else) as false. This allows you to check if the variable has a defined value and respect the values of 0, ‘’, and false, instead of treating them as falsey and using the default value instead.

let isUserActive = user.active ?? true;

let divHeight = height ?? “Please enter a height”;

function convertToString(s: string | undefined): string {

 return s ?? ‘hello world’;

}
  1. Typing object properties

    Typing object keys {name: string, age: number} is one of the more accessible and common parts of typescript. However, it took some time in my professional career before I saw the typing of object property names. The syntax makes sense, but it is something I’m sure I never would have guessed without seeing it in use.

This is the syntax for setting individual property types:

`type User = {[userName: string]: string | undefined};`

This is the syntax for checking a property type as a property of a specific object - ‘keyof T’ checks that that propertyName is a key within object T:

function getProperty<T>(obj: T, propertyName: keyof T) {

  return obj[propertyName];

}

This is the syntax for checking the property type of a specific object, written in a syntax different from the above example. This example checks that the key K exists as a key of object T (and then the function makes it optional).

type Optional<T> = { [K in keyof T]?: T[K] };
  1. Record<Keys,Type> - when you don’t know what the exact properties will be

    Sometimes, you have a general idea of what data you’ll be getting back from a back-end call but don’t know the specific properties you may be receiving. In instances like these, TypeScript’s Record type is very helpful.

Record<Keys, Values> is an object type that takes a type for Keys, and a second type for values. For example:

Record<string,string> - {'a': 'b', ‘c’: ‘d’}Record<string, number> - {‘a’: 1, ‘b’: 2}

  1. Overload signatures - paired function parameter and return types

Note: Arrow functions do not support overloading. You will have to change the syntax of an arrow function if you would like to use this feature

Imagine you want to write a function that doubles a passed-in number. You also want this function to be able to double every number in an array of numbers. So the function accepts either a number or an array of numbers, and returns either a number or array of numbers.\

function double(x: number | number[]): number | number[] {

	If (typeof x === number){

		return x*2

} else {

x.map(n => n*2)

}

}

In situations like this, we want to pair the return types with the parameter type - if we are passing in a number, we are returning a number, and if we pass in an array of numbers, we return an array of numbers, but we cannot return a number if we pass in an array of numbers or vice versa.

We are able to pair the parameter and return types by using overload signatures. To do this, we declare function signatures separately from the full function definition. A function signature defines a function’s input parameters and types, as well as their return types. Here is an example using overload signatures (the first two lines are function signatures):

function double(x:number):number;

function double(x: number[]): number[];

function double(x: number | number[]): number | number[] {

	if (typeof x === number){

	    return x*2

        } else {

            return x.map(n => n*2)

        }

}

  1. Default function parameter types

    Having a default parameter value for an optional parameter is a common part of development. For example, in this function, you can pass in two numbers to multiply together, but if the user only passes in one number, it will default to being multiplied by two:

function multiply(a, b = 2) {

 	 return a * b;

}

TypeScript has a similar feature that allows you to set a default type for a parameter if a type is not set. For instance, we might have an online store, where we can usually count on data about prices to be numbers, but have some external calls or legacy code where prices are saved as strings. In this instance, we have our PriceArray set with a default type of number, but the ability to manually set an array to strings if necessary:\

type PriceArray<T=number> = Array<T>;

const CandyPrices: PriceArray = [2,5,9];

const VendorPrices: PriceArray<string> = [‘2’,’5’,’9’]
  1. Conditional return types based on options

It’s possible a function we write may return something different depending on whether optional parameters are included or not. In these cases, we want to be able to set the correct return types if those optional parameters are included.

For instance, imagine we are fetching a specific user’s data. We might want an option to retrieve only minimal user data and another option that allows us to include some user details, such as data to display for other users, such as their nickname and avatar. Rather than writing two separate functions, we can have an optional parameter of “displayData.” We can make two separate types - the plain User type and a UserWithDisplayData type, and use function overload signatures to set the return type based on the displayData option being set to true. IE:

Interface User {

Id: string

Email: string

}

Interface UserWithDisplayData extends User {

	Nickname: string

	avatarUrl: string

}

Function fetchUser(id: number): User;

Function fetchUser(id: number, {options: {withDisplayData: true}): UserWithDisplayData;

function fetchUser(id: number, options: {withDisplayData?: false}): User | UserWithDisplayData {

 //call to fetch user info

}

Given TypeScript’s popularity, it’s an essential skill for modern front-end developers to have. These features have helped me use TypeScript more effectively, spend less time trying to figure out how to even describe the issue I have to be able to search Google for documentation, and spend less time chasing down TS errors. Hopefully, some of these will be helpful to you!



Written by danielleford04 | Front end engineer. Meditator. Soccer fan.
Published by HackerNoon on 2024/04/03