DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 970,177 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Jumpstart on advance typescript Part 1
Manish Kumar
Manish Kumar

Posted on • Updated on

Jumpstart on advance typescript Part 1

I started learning Typescript couple of months back and initially, it was quite overwhelming for me because I used to write plain old javascript where no strict type checking, no interfaces, no generic types. Seeing these feature/keywords in a large project was not recognized by my mental modals for initial few days at that moment just only one way I have left to do is to learn those jargons and make them my friend

In this blog, I will try to explain some of Typescript's advance feature which is I learned during this process and are very useful. The main idea is to explain it in the simplest way possible. I am also assuming you already have a basic understanding of how strict type system works.

βš™οΈ Generics

A good piece of code is that which is reusable and flexible. Generics in Typescript provides a way to write components which can work over various kind of data type rather than a single one.

If we are performing some operation on more then one data types we don't need to repeat same code block for each data type. Generics allow us to do so while keeping it type-safe at same time.

Let's see an example where we are just returning what we have passed.

// Will return string
function rebound(arg: string): string {
    return arg
}

// Will return number
function rebound(arg: number): number {
    return arg
}
Enter fullscreen mode Exit fullscreen mode

We can combine this two functions using any type

function rebound(arg: any): any {
    return arg
}
Enter fullscreen mode Exit fullscreen mode

but as you already know any is not type safe, it means compiler can't catch error on compile time.

Now lets make it type safe using Generics

function rebound<T>(args: T): T {
    return args
}
Enter fullscreen mode Exit fullscreen mode

You must be thinking

Wait the heck is <T> or T

These are called Type parameters. These lets users to use this function with any type. It tells typescript which data-type argument is passed. let see the example of how to use it.

const message = rebound<string>('Hello, World'); // OK

const count = rebound<number>(200); // OK

const count = rebound<number>('200'); // Error, data-type expected is number but passed string
Enter fullscreen mode Exit fullscreen mode

You can use multiple type parameters as well

function someFunction<T, U>(arg1: T, args2: U) { ... }
Enter fullscreen mode Exit fullscreen mode

Not only functions, but Classes and interfaces can be Generic as well. let's see below example where a generic interface is used as type

Generic interface as Type

interface KeyValuePair<T, U> {
    key: T;
    value: U;
}

let keyPair1: KeyValuePair<number, string> = { key:1, value:"Manish" }; // OK
let keyPair2: KeyValuePair<number, number> = { key:1, value:1988 }; // OK

Enter fullscreen mode Exit fullscreen mode

As you can see in the above example, by using generic interface as type, we can specify the data type of key and value

Generic interface as Function Type

In same way, we can use interface as a function type.

interface KeyValueLogger<T, U>
{
    (key: T, val: U): void;
};

function logNumKeyPairs(key:number, value:number):void { 
    console.log('logNumKeyPairs: key = ' + key + ', value = ' + value)
}

function logStringKeyPairs(key: number, value:string):void { 
    console.log('logStringKeyPairs: key = '+ key + ', value = ' + value)
}

let numLogger: KeyValueLogger<number, number> = logNumKeyPairs;
numLogger(1, 12345); //Output: logNumKeyPairs: key = 1, value = 12345 

let strLogger: KeyValueLogger<number, string> = logStringKeyPairs;
strLogger(1, "kumar"); //Output: logStringKeyPairs: key = 1, value = Bill 
Enter fullscreen mode Exit fullscreen mode

Generic classes

So we learned about generic functions and interfaces. Similar way we can create generic classes too. Let's see below example

interface KeyValuePair<T, U> {
    key: T;
    value: U;
}

class Dictionary<T, U> {

    private keyValueCollection: Array<keyValuePair<T, U>>

    add(key:T, value: U) {
        if(this.keyValueCollection.findIndex(pair =>  pair.key === key) > -1) {
            throw new Error(key + ' key already exist in dictionary');
        }
        this.keyValueCollection.push({key, value});
    }

    remove(key: T) {
        const index = this.keyValueCollection.findIndex(pair =>  pair.key === key);

        if(index > -1) {
            this.keyValueCollection.splice(index, 1);
        } else {
            throw new Error(key + ' key does not exist in dictionary');
        }
    }
}

const dictionary = new Dictionary<string, string>();
dictionary.add('name', 'john');
dictionary.add('country', 'India');

Enter fullscreen mode Exit fullscreen mode

You can see in the above example, I have used both generic interface and generic class, which gives us flexibility to create a HashMap with custom implementation.

β›“Conditional Type

Conditional type is something which we are not going to use very frequently directly but we use them indirectly a lot of time. In very simple terms it is a kind of ternary expression for Types.
Let's look at a very basic example

type BookPages = bookId extends string[] ? number[] : number;
Enter fullscreen mode Exit fullscreen mode

in above example, we are first check if bookId variable is a array of string, if yes then we will make BookPages array of number otherwise the only number

according to Typescript definition of conditional types

"A conditional type selects one of two possible types based on a condition expressed as a type relationship test"

Filtering using conditional type

As we already seen a basic example lets see some more examples to get a better understanding. Let's create a filter type which will select a subset of a union type

type Filter<T, U> = T extends U ? never : T;

type CarTypes = 'SUV' | 'HATCHBACK' | 'SEDAN' | 'MUSCLE' | 'VINTAGE';

type NonVintageCarTypes = Filter<CarTypes, 'VINTAGE'>; // 'SUV' | 'HATCHBACK' | 'SEDAN' | 'MUSCLE'
Enter fullscreen mode Exit fullscreen mode

The conditional type here is just going to check each string literal in string literal union parameter (T in this case), is assignable to a string literal union under the second type parameter (U in this case). If yes, never is returned which means no result otherwise string literal is added to resulting union

In the above example, we filtered out one of union type from CarType and created a new type NonVintageCarTypes.

Basically this is how is in-built Exclude type is defined. So at this point, we can create Include type very easily, we just need to reverse the condition

type Include<T, U> = T extends U ? T : never;
Enter fullscreen mode Exit fullscreen mode

Now we get some understanding about conditional type, hence it won't be very tough to create a copy of built-in NonNullable type using conditional type

type MyNonNullable<T> = T extends null ? never : T;

type SomeNullableType = string | number | null;

type SomeNonNullableType = MyNonNullable<SomeNullableType>;
Enter fullscreen mode Exit fullscreen mode

Replace overloading with conditional types

Suppose there is an entertainment center which maintains a database of Books and TV series as below

interface Book {
  id: string;
  name: string;
  tableOfContents: string[];
}

interface TvSeries {
  id: number;
  name: string;
  Episode: number;
}

interface IItemService {
  getItem<T>(id: T): Book | Tv;
}

let itemService: IItemService;
Enter fullscreen mode Exit fullscreen mode

In IItemService interface, getItem function can return detail of Book or TvSeries. If you look closely we have id as string then we need to return Book and if it is number we need to return TvSeries. We could achieve this with overloading as below and it works perfectly

interface IItemService {
  getItem(id: string): Book;
  getItem(id: number): Tv;
  getItem<T>(id: T): Book | Tv;
}
Enter fullscreen mode Exit fullscreen mode

But using conditional types we can keep only one definition. Let's see below change

interface IItemService {
  getItem<T>(id: T): T extends string ? Book : Tv;
}
Enter fullscreen mode Exit fullscreen mode

Isn't it very handy!

There are many ways we can use conditional type in conjunction with other Typescript fundamentals. I will try to cover some of those in my next blog. Thanks for reading it

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Just kidding, it's a personal preference. But you can change your theme, font, etc. in your settings.

The more you know. 🌈