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
bookShelf.addItemToCatalog(new Book())
bookShelf.addItemToCatalog(new Magazine()) [X]
let companies: Array
let primeNums: Array
let lastValue = primeNums.pop(); //lastValue type is number
Understanding function type parameters
======================================
Genericfunctions.ts
function checkOut
availableItem: T = getItemFromDB(item);
if(availableItem){
// check out item for customer
}
return item;
}
checkOut
MultipleTypeParameters.ts
————————–
function checkOut
availableItem: T = getItemFromDB(item);
activeCustomer: V = getCustomerFromDB(customer);
if(availableItem && activeCustomer){….}
return item;
}
checkOut
Creating Generic Functions
function shortenArray
return data.splice(amountToShorten, data.length);
}
let stringArray: string[] = [‘VB’, ‘C++’, ‘Javascript’]
let fewerLanguages: Array
//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
let bigRooms: Array
rooms.forEach(r => {
if(r.capacity > minSize){
bigRooms.push(r)
}
})
return bigRooms;
}
let bigRooms: Array
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
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:
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
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
_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
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
// let myNumberStack = new Stack
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.