Creating and using Generics in Typescript

Creating and using Generics in Typescript
Github

1. Understanding and Applying Built-in Generics
2. Generic Functions
3. Generic Interfaces and Classes

What are Generics ?
– Reusable code that works with multiple types
– May be functions, interfaces, or classes
– Code that accepts type parameters for each instance or invocation

Adding T after class name is generic class and when i create instance of the class, i’ll need to speicify what type should be used in place of T everywhere T appears in the class definition.
//Notice T is aslo used as a type annotation on the internal array T[]

The type i speicify only applies to that instance of the class. I could create another instance with a different type for the type parameter, and the array and methods only work with that type.

MagazineShelf.ts

import { Magazine } from ‘./Magazine’
export class MagazineShelf {
_magazines: Magazine[] = [];
addMagazineToCatalog(newMagazine: Magazine){}
removeMagazineFromCatalog(oldMagazine: Magazine){}
}

BookShelf.ts

import { Book } from ‘./Book’
export class BookShelf {
_books: Book[] = [];
addMagazineToCatalog(newBook: Book){}
removeMagazineFromCatalog(oldBook: Book){}
}

Shelf.ts

export class Shelf {
_items: T[] = [];
addItemToCatalog(newItem: T){…}
removeItemFromCatalog(oldItem: T) {…}
}

import { Shelf } from ‘./Shelf’;
import { Book } from ‘./Book’;
import { Magazine } from ‘./Magazine’

let bookShelf: Shelf = new Shelf();
bookShelf.addItemToCatalog(new Book())
bookShelf.addItemToCatalog(new Magazine()) [X]

let companies: Array = [‘Microsoft’, ‘Google’, ‘Amazon’]
let primeNums: Array = [7,8,9]
let lastValue = primeNums.pop(); //lastValue type is number

Understanding function type parameters
======================================
Genericfunctions.ts

function checkOut(item: T): T {
availableItem: T = getItemFromDB(item);
if(availableItem){
// check out item for customer
}

return item;
}

checkOut(someBook)

MultipleTypeParameters.ts
————————–
function checkOut(item: T, customer: V) : T {
availableItem: T = getItemFromDB(item);
activeCustomer: V = getCustomerFromDB(customer);
if(availableItem && activeCustomer){….}
return item;
}

checkOut(someBook, someStudent)

Creating Generic Functions

function shortenArray(data: Array, amountToShorten: number): Array {
return data.splice(amountToShorten, data.length);
}

let stringArray: string[] = [‘VB’, ‘C++’, ‘Javascript’]
let fewerLanguages: Array = shortenArray(stringArray, 2)

//we can also call this generic function
let fewerLanguages = shortenArray([1,2,3], 2)

Applying Type Constraints to Generic Functions

models/meetingResource.ts
export interface MeetingResource {
name: string;
capacity: number;
}

import {MeetingResource} from “./meetingResource”
export class ConferenceRoom implements MeetingResource {
name: string;
capacity: number;
hasProjector: boolean;
location: string;
}
export const conferenceRoomData: ConferenceRoom[] = [
{name: ‘Cheerios’, capacity: 15, hasProjector: false, location: ‘HQ’},
{name: ‘Froot Loops’, capacity: 25, hasProjector: true, location: ‘East Campus’},
]

partyTent.ts

import {MeetingResource} from “./meetingResource”
export class PartyTent implements MeetingResource {
name: string;
capacity: number;
companyOwned: boolean;
tablessIncluded: number;
}

export const partyTentData: PartyTent[] = [
{name: ‘Parasol’, capacity: 15, companyOwned: true, tablessIncluded: 3},
{name: ‘Shady’, capacity: 105, companyOwned: false, tablessIncluded: 10}
]

models/building.ts

export class Building {
address: string;
numberOfFloors: number;
}

export const buildingData: Building[] = [
{address: ‘Main Street’, numberOfFloors: 10},
{address: ‘Central Avenue’, numberOfFloors: 2}
]

main.ts – In this file Type constraints for capacity through extends
import {MeetingResource} from ‘./models/meetingResource’;
import {ConferenceRoom, conferenceRoomData} from ‘./models/conferenceRoom’;
import {PartyTent, partyTentData} from ‘./models/partyTent’;
import {Building, buildingData} from ‘./models/building’;

function getBigRooms (rooms: Array, minSize: number) : Array{
let bigRooms: Array = [];
rooms.forEach(r => {
if(r.capacity > minSize){
bigRooms.push(r)
}
})
return bigRooms;
}

let bigRooms: Array = getBigRooms(conferenceRoomData, 20);

let bigRooms = getBigRooms(conferenceRoomData, 20) // valid inference
let bigRooms = getBigRooms(partyTentData, 20)

// Here bigRooms is called non-generic function type

let bigRooms = getBigRooms(buildingData, 20) // ERROR

This type missing following properties from type ‘MeetingResource’ : name, capacity
Building class does not satisfy the constraint i placed on the functions’s type parameter. i prefered to use more contraints to satisfy our intend

Using Generic Function Types

Knowing how to use function types in your code allows you to declare variables that can be assigned functions with the same signature, but different behaviors.

The type of the non-generic function includes the number and types of the function parameters, along with the type of the function’s return value.

Generic function types are similar, except they also include all of the type parameters defined on the function.

Example : the type of getBigRooms function is
(rooms: Array, minSize: number) : Array

It takes one type parameter that extends the MeetingResource interface, and it will be passed an array containing that tyep along with a number and will return a different array of that type , we can assign this type to variable

let getLargeRooms: (rooms: Array, minSize: number) => Array ;

the only change compare to above example is instead of “:” we use “=>” for the return type

I can now assign any generic function to this variable as long as it shares the same signature.

Ex : getLargeRooms = getBigRooms ;

And i can call like previously

let largeRooms = getLargeRooms(conferenceRoomData, 30 )

Other than nothing different from non-generic function types.

One more thing , even though those placeholders are different, the types are still compatible; In the above both the getBigRooms, getLargeRooms both use the same type parameter and the same names for the function parameters. That’s not required.

Example :
function shortenArray(data: Array, amountToShorten: number): Array {
return data.splice(amountToShorten, data.length)
}

let shrinkArray: (original: Array, units: number) => Array;
shrinkArray = shortenArray;

If you go on internet and search how to use Typescript generics, most of what you’ll find will be examples that use classes. But generics work great with functions as well. In fact, a single generic function may be all you need in many cases. Like all generic, they give you the versatility to work with multiple types, but maintain a high degree of type saftey. When you constrain the types that can be applied to your generic functions, you increase their practicallity. Generic functions are great for small pieces of funtionality, but there will be times when you need to create your own generic types that may have lots of generic methods and properties. In the next will see about it.


Understanding Generic Interfaces And Classes

The only difference in this interface and a non‑generic interface is the type parameter in angle brackets just after the interface name.

Here on my library manager interface, I’ve declared an items property that will be an array containing objects of type T. The type that will be used in place of T will be specified when the interface is implemented. That could either be with an object literal or with a class.
Perhaps the simplest example I can show you is to declare a new variable to have the interface type and assign an object literal to it.
libraryCollection.ts
export interface LibraryCollection {
_items: T[];
addItemToCatalog(newItem: T): void;
removeItemFromCatalog(oldItem: T): void;
}

let titleCollection: LibraryCollection = {
_items: [“Winnie the Pooh”, “Curious George”]
addItemToCatalog: s => console.log(`Added item ${s}`)
removeItemFromCatalog: s => console.log(`Removed item ${s}`)
}

The other way to implement a generic interface is with a generic class. You’re hopefully already familiar with how to implement a non‑generic interface with a class. The code for the generic version, as you might imagine, is very similar.

shelf.ts
import {LibraryCollection} from “./libraryCollection”
export class Shelf implements LibraryCollection {
_items: T[];
addItemToCatalog(newItem: T): void {….}
removeItemFromCatalog(oldItem: T): void {….}
}

The important things to note here are that the class has one type parameter T, and it implements the LibraryCollection of T interface we just saw.

Because the class will implement the interface, the type parameter I specify when I create a new instance of the class will be the type used with the property and methods I define on the interface. Therefore, the type parameter for the class needs to be the same as the type parameter on the interface. I could include additional type parameters on the class if I wanted, but at least one of them must match the type parameter on the interface the class implements.

Creating a Generic Interface and Class

stack.ts
interface DataStructure{
push(newItem: T): void;
pop(): T;
}
class Stack implements DataStructure {
items: Array = [];
push(newItem: T): void {
this.item.push(newItem)
}
pop(): T {
return this.items.pop();
}
peek(): T {
return this.items[this.items.length-1]
}
}

let myNumberStack: Stack = new Stack();
// let myNumberStack = new Stack(); //Valid

myNumberStack.push(1);
myNumberStack.push(2);
myNumberStack.push(3);

console.log(myNumberStack.pop()); //3
console.log(myNumberStack.peek()); //2
console.log(myNumberStack.pop()); //2
console.log(myNumberStack.pop()); //1

Applying Type Constraints to a Generic Class

I’m going to create a new generic class that I can use with the model classes I’ve already got in the models folder of my project. I want it to represent a room reservation, so I’ll add a new file named reservation.ts. Inside it, I’ll create a new generic class named Reservation that takes a single type parameter, T.
reservation.ts
class Reservation {
reservationDate: Date;
organizerName: string;
resource: T;

requestResouce(requestResouce: T, requester: string) {
this.resource = requestResouce;
this.organizerName = requester;
console.log(`${requester} requested a reservation ${requestResouce.name}`) // name shows error
}
}

The first two lines just take the values passed to the methods and assigned them to the resource and organizerName properties on the instance.

I don’t currently have any constraints on what type can be substituted for T when instances of this class are created. That means the compiler can’t assume whatever type that is will have a name property.

The MeetingResource interface I showed you earlier in the course has a name property, and since I know I only want to use this code with classes that extend that interface, I’ll add a constraint to the type parameter that says T must extend MeetingResource.

reservation.ts
import { MeetingResource } from ‘./meetingResource’;
import { ConferenceRoom, conferenceRoomData } from ‘./conferenceRoom’;
class Reservation {
reservationDate: Date;
organizerName: string;
resource: T;

requestResouce(requestResouce: T, requester: string) {
this.resource = requestResouce;
this.organizerName = requester;
console.log(`${requester} requested a reservation ${requestResouce.name}`) // no error
}
}

let teamMeeting = new Reservation();
teamMeeting.requestResouce(conferenceRoomData[0], ‘Gary’)

//Static members can not reference class type parameters
static requestResouce(requestResouce: T, requester: string){}
Note: Generic classes are only generic over their instances
Since static members apply to the class itself and not instances of the class, you can’t use generics on static members.
If i want to keep the methods to use on instances. we have to remove static.

Leave a Reply

Your email address will not be published. Required fields are marked *