Jumpstart on advance typescript Part 1
Manish Kumar
Posted on May 15, 2020
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
}
We can combine this two functions using any
type
function rebound(arg: any): any {
return arg
}
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
}
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
You can use multiple type parameters as well
function someFunction<T, U>(arg1: T, args2: U) { ... }
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
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
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');
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;
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'
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;
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>;
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;
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;
}
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;
}
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
Posted on May 15, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 3, 2024