Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Typescript

TypeScript Tips and Tricks

5.00/5 (1 vote)
17 Jan 2022CPOL7 min read 6.2K  
How TypeScript improves coding in JavaScript
In this article, we explore how TypeScript improves JavaScript coding.

Introduction

TypeScript is a superset of JavaScript. It’s like JavaScript, but with superpowers.

Vanilla JavaScript is a dynamically typed language. For example, if you assign a number type to a variable, then assign a string to that same variable further in the code, JavaScript compiles just fine. You’ll only get an error when something breaks in production.

If you're set against using static typing tools like TypeScript, you can use JavaScript linters to provide some type checks. A linter would have helped catch the error in our example. Linters have their limits, however. For instance, linters don’t support union types — we’ll explore this further in the article — and they also can’t lint complex object structures.

TypeScript provides more type features to JavaScript, enabling you to structure your codebase better. It type-checks your codebase during compile-time and helps prevent errors that may make it to production.

TypeScript improves JavaScript development in many different ways. But in this article, we’ll zoom in on six areas, including helpful tips and tricks for using TypeScript. Some of these tips will discuss the ways TypeScript supports functional programming.

Read-Only Types

Functional programming generally demands immutable variables — and by extension, immutable objects as well. An object with four properties must have the same properties throughout its life, and the values of those properties can’t change at any point.

TypeScript makes this possible with the Readonly utility type. Here’s a type without it:

TypeScript
...  
type Product = {  
    name: string  
    price: number  
}

const products: Product[] = [  
{  
    name: "Apple Watch",  
    price: 400,  
},  
{  
    name: "Macbook",  
    price: 1000,  
},  
]

products.forEach(product => {  
    // mutating here  
    product.price = 500  
})  
...

In the code, we mutated the price property. Since the new value is a number data type, TypeScript doesn’t throw an error. But using Readonly, our code is as follows:

TypeScript
...  
const products: Readonly<Product>[] = [  
{  
    name: "Apple Watch",  
    price: 400,  
},  
{  
    name: "Macbook",  
    price: 1000,  
},  
]

products.forEach(product => {  
    // mutating here  
    product.price = 500  
})  
...

Image 1

As we can see in this screenshot, the price property is read-only and can’t be assigned another value. Attempting to mutate this object’s value will throw an error.

Union Types

TypeScript allows you to combine types in compelling ways. The combination of two or more types is called a union type. You use the "|" symbol to create a union type. Let’s look at some examples.

Number and String Union Type

Sometimes, you need a variable to be a number. Other times, you need the same variable to be a string.

With TypeScript, you can achieve this by doing something as simple as:

TypeScript
function(id: number | string) {  
    ...  
}

The challenge with the union type declared here is you can’t call id.toUpperCase because TypeScript doesn’t know if you’re going to pass a string or a number during the function declaration. So, to use the toUpperCase method, you must check if the id is a string using typeof === "string".

But, if it can’t apply a standard method to all members that make a particular union, TypeScript won’t complain.

Limiting Acceptable Types

With unions, you can also limit a variable’s acceptable data-type values. You do this using literal types. Here’s an example:

TypeScript
function(type: "picture" | "video") {  
    ...  
}

This union type comprises the string literal types of picture and video. This code causes other string values to throw an error.

Discriminated Unions

Another positive thing about unions is that you can have object types of different structures, each with a common distinguishing property. Here’s an example:

TypeScript
...  
type AppleFruit = {  
    color: string;  
    size: "small" | "large"  
}

type OrangeFruit = {  
    isRipe: boolean;  
    count: number;  
}

function describeFruit(fruit: AppleFruit | OrangeFruit) {  
...  
}  
...

In this code, we have a union type, fruit, made of two different object types: AppleFruit and OrangeFruit. The two types of fruits have no properties in common. This difference makes it difficult for TypeScript to know what fruit is when we use it, as the following code and screenshot illustrate:

TypeScript
...  
function describeFruit(fruit: AppleFruit | OrangeFruit) {  
    if (fruit.color) {  
        // throw error...see Figure B.  
    }  
}  
...

Image 2

The error in this screenshot shows that color doesn’t exist on the orange type. There are two solutions to this.

The first solution is to check, in a more acceptable way, if the color property exists. Here’s how:

TypeScript
...  
function describeFruit(fruit: AppleFruit | OrangeFruit) {  
    if ("color" in fruit) {  
        // now typescript knows fruit is of the apple type  
    }  
}  
...

We check if the color property is in the fruit object. Using this check, TypeScript can correctly infer the type, as this screenshot shows:

Image 3

The second solution is to use discriminated union. This method implies having a property that clearly distinguishes both objects. TypeScript can use that property to know which type is being used at that particular time. Here’s how:

TypeScript
...  
type AppleFruit = {  
    name: "apple";  
    color: string;  
    size: "small" | "large";  
}

type OrangeFruit = {  
    name: "orange";  
    isRipe: boolean;  
    count: number;  
}

function describeFruit(fruit: AppleFruit | OrangeFruit) {  
    if (fruit.name === "apple") {  
        // apple type detected  
    }  
}  
...

Since both types have the name property, fruit.name won’t throw an error. And, using the value of the name property, TypeScript can determine the type of fruit.

Intersection Types

In contrast to union types, which involve type1, type2, _or_ type3, intersection types are type1, type2 and type3.

Another significant difference between these types is that while union types can be strings or numbers, intersection types can’t be strings and numbers. Data can’t be a string and a number at the same time. So, intersection types involve objects.

Now that we’ve discussed the difference between union and intersection types, let’s explore some ways of doing intersections:

TypeScript
...  
interface Profile {  
    name: string;  
    phone: string;  
}

interface AuthCreds {  
    email: string;  
    password: string;  
}

interface User: Profile & AuthCreds  
...

Profile and AuthCreds are examples of interface types that exist independently of each other. This independence means you can create an object of type Profile and another of type AuthCreds, and these objects may not be related together. However, you can intersect both types to make a bigger type: User. This type’s structure is an object with four properties: name, phone, email and password, all of the string type.

Now you can create a User object like this:

TypeScript
...  
const user:User = {  
    name: "user";  
    phone: "222222",  
    email: "user@user.com"  
    password: "***"  
}  
...

TypeScript Generics

Sometimes, when you create a function, you know its return type. Here's an example:

TypeScript
...  
interface AppleFruit {  
    size: number  
}

interface FruitDescription {  
    description: string;  
}

function describeFruit(fruit: AppleFruit): AppleFruit & FruitDescription {  
    return {  
        ...fruit,  
        description: "A fruit",  
    }  
}

const fruit: AppleFruit = {  
    size: 50  
}

describeFruit(fruit)  
...

In this example, the describeFruit function takes in a fruit parameter of the AppleFruit type. It returns an intersection type made of the AppleFruit and FruitDescription types.

However, what if you wanted this function to return descriptions for different fruit types? Generics are relevant here. Here’s an example:

TypeScript
...  
interface AppleFruit {  
    size: number  
}

interface OrangeFruit {  
    isOrangeColor: boolean;  
}

interface FruitDescription {  
    description: string;  
}

function describeFruit<T>(fruit: T): T & FruitDescription {  
    return {  
        ...fruit,  
        description: "A fruit",  
    }  
}

const appleFruit: AppleFruit = {  
    size: 50  
}

describeFruit(appleFruit)

const orangeFruit: OrangeFruit = {  
    isOrangeColor: true  
}

describeFruit<OrangeFruit>(orangeFruit)  
...

The generic function describeFruit accepts different types. The code determines the type of fruit to pass when it calls the function.

The first time we call describeFruit, TypeScript automatically infers T to be AppleFruit because appleFruit is of that type.

The next time, we specify the T’s type to be OrangeFruit using "OrangeFruit" before calling the function.

These lines do the same thing, but, in some cases, the automatic inference may not be accurate.

We can pass different types to that function in our example, which simply returns an intersection of FruitDescription and the type we passed.

Here’s an example of passing a type to a function using generics:

Image 4

The describeFruit function has a type, as we originally defined it with OrangeFruit.

Path Aliases with TypeScript

You might usually import like this:

TypeScript
...  
import Button from "../../../../components/Button"  
...

This command to import may be in different files that require this component. When you change the location of the "Button" file, you also need to change this import line in the various files that use it. This adjustment also results in more file changes to track in version control.

We can improve how we import by using alias paths.

TypeScript uses the "tsconfig.json" file to store the configurations that enable it to work as you want. Inside, there’s the paths property. This property lets you set path aliases for different directories in your application. Here’s how it looks, using compilerOptions, baseUrl, and paths:

TypeScript
...  
{  
    "compilerOptions": {  
        "baseUrl": ".", // required if "paths" is specified.  
        "paths": {  
            "components/*": ["./src/components/*"] // path is relative to the baseUrl  
        }  
    }  
}  
...

With the components alias, you can now import like this:

TypeScript
...  
import Button from "components/Button"  
...

Regardless of how deep you are in a directory, using this command correctly resolves the "Button" file.

Now, when you change your component’s location, all you must do is update the paths property. This method means more consistent files and fewer file changes for version control.

Using Libraries with Built-In TypeScript Support

When you use libraries without TypeScript support, you miss TypeScript’s benefits. Even a simple mistake — such as using the wrong data types for an argument or object property — may cause you headaches without the warning TypeScript provides. Without this alert, your app might crash because it expected a string, but you passed a number instead.

Not every library has TypeScript support. When you install a library that does support TypeScript, it installs the distributed code with TypeScript declaration files.

Conclusion

In this article, we’ve explored how TypeScript improves JavaScript coding. We’ve also discussed how TypeScript supports some functional programming techniques. This ability makes TypeScript suitable for object-oriented programming (OOP) developers and functional programmers.

As a next step, you can dive into TypeScript’s config options. TypeScript provides this exhaustively thorough resource to help you build your next app.

History

  • 17th January, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)