Photo by Bernard Hermant on Unsplash
Polymorphism
Polymorphism in other languages like Java, C++ can also represent itself in the form of Typescript( AKA: enhancement of Javascript eco-system)
Let's talk about concrete type
boolean
string
Date[]
{a:number} | {b:string}
(numbers:[]numbers)=>number
Concrete types are useful when we know precisely what type we're expecting, and want to verify that type was actually passed. But sometime,we don't know it
Generic type on fn
type Filter = {
(array:number[],f:(item:number)=>boolean):number[];
(array:string[],f:(item:string)=>boolean):string[];
(array:object[],f:(item:object)=>boolean):object[];
}
const filter:Filter =(arr:any[],item:(any)=>boolean):any[]=>{
// Implementation
}
let names = [
{firstName: 'beth'}, {firstName: 'caitlyn'}, {firstName: 'xin'}
]
let result = filter( names,
_ => _.firstName.startsWith('b')
)
// Error TS2339: Property 'firstName' does not exist on type 'object'.
result[0].firstName // Error TS2339: Property 'firstName' does not exist
// on type 'object'.
A better way to do it is through GENERICS
type Filter = {
<T>(array:T[],(item:T)=>boolean)=>T[];
}
T is just a type name, and we could have used any other name instead: A, Zebra, or l33t. By convention, people use uppercase single-letter names starting with the letter T and continuing to U, V, W, and so on depending on how many generics they need.
If you’re declaring a lot of generics in a row or are using them in a complicated way, consider deviating from this convention and using more descriptive names like Value or WidgetType instead.
Generic with function overloaded
Explicit generic
type Filter<T>={
(array:T[],(item:T)=>boolean):T[];
}
// Generic types Filter requires 1 type argument
let filter:Filter<string> = (array,f)=> {...}
Where can we declare generics
//Full-call signature
// Because function is also an object so we can declare it as a object
// with the () as the function's signature
type Filter = {
<T>(arr:T[],f:(item:T)=>boolean):T[];
}
interface Filter = {
<T>(arr:T[],f:(item:T)=>boolean):T[];
}
let filter:Filter;
// Explicity generic full-call signature
type Filter<T> = {
(arr:T[],f:(item:T)=>boolean):T[];
}
let filter:Filter<string>;
//Shorthand call signature
type Filter = <T>(array:T[],f:(item:T)=>boolean):T[];
let filter:Filter;
//Explicity generic Shorthand call signature
type Filter<T> = (array:T[],f:(item:T)=>boolean):T[];
let filter:Filter<string>;
//A named function call signature
function filter<T>(array:T[],f:(item:T)=>boolean):T[] {
//Implementation
}
Let's implement a map with TS
function map<T,U>(array:T[],f:(item:T)=>U):U[]{
let result = [];
for(let i=0;i<array.length;i++) {
result[i] = f(array[i]);
}
return result;
}
Standard library
interface Array<T> {
filter(
callbackfn: (value: T, index: number, array: T[]) => any,
thisArg?: any
): T[];
map<U>(
callbackfn: (value: T, index: number, array: T[]) => U,
thisArg?: any): U[]
}
interface ArrayConstructor {
new (arrayLength?: number): any[];
new <T>(arrayLength: number): T[];
new <T>(...items: T[]): T[];
(arrayLength?: number): any[]; // This could potentially [1,2,"a"];
<T>(arrayLength: number): T[];
<T>(...items: T[]): T[];
isArray(arg: any): arg is any[];
readonly prototype: any[];
}
https://github.com/microsoft/TypeScript/blob/main/src/lib/es5.d.ts
Lodash
interface LodashEvery {
<T>(predicate: lodash.ValueIterateeCustom<T, boolean>): LodashEvery1x1<T>;
<T>(predicate: lodash.__, collection: lodash.List<T> | null | undefined): LodashEvery1x2<T>;
<T>(predicate: lodash.ValueIterateeCustom<T, boolean>, collection: lodash.List<T> | null | undefined): boolean;
<T extends object>(predicate: lodash.__, collection: T | null | undefined): LodashEvery2x2<T>;
<T extends object>(predicate: lodash.ValueIterateeCustom<T[keyof T], boolean>, collection: T | null | undefined): boolean;
}
Generic type aliases
type MyEvent<T> = {
target:T;
type:string;
}
type ButtonEvent = MyEvent<HTMLButtonElement>;
let myEvent : ButtonEvent = {
target:document.querySelector("#myButton");
type:"click"
}
More use-case for Generic type aliases
Object type
type Partial<T> = {
[K in keyof T>?:T[K];
}
type Required<T> = {
[K in keyof T> -?:T[K];
}
type ReadOnly<T>= {
readonly [K in keyof T>:T[K];
}
type Record<K extends keyof any,V> = {
[Key in K]:V
}
type Pick<T,Key extends keyof T> = {
[K in Key]:T[K];
}
type Omit<T,K extends keyof T> =Pick<T,Exclude<keyof T,K>>;
Union type
type Extract<T,U> = T extends U ? T : never;
type Exclude<T,U> = T extends U ? never : T;
never | never | "a" => "a"
Bounded Polymorphism
Let's design a TreeNode type and its sub-type
type TreeNode = {
value:string;
}
type LeafNode = TreeNode & {
isLeaf:true;
}
type InnerNode = TreeNode & {
children: [TreeNode] | [TreeNode,TreeNode];
}
Let's write a generic function thats take TreeNode type but also its subtype
function mapNode<T extends TreeNode>(node:T,f:(value:string)=>string):T{
return {
...node,
value:node.value;
}
}
Bounded polymorphism with multiple constraints
type HasSides = { numberOfSides:number}
type SidesHaveLength = { sideLength:number }
function logParameter<Shapes extends HasSides & SidesHaveLength>(
s:Shape):Shape {
console.log(s.numberOfSides,s.sideLength);
return s;
}
Let's implement a call function
call is a variadic function (as a reminder, a variadic function is a function that accepts any number of arguments)
function call<T extends unknown[],R>
(f:(...args:T)=>R,
...args:T):R {
return f(...args);
}
Generic type default
type MyEvent<T extends HTMLElement = HTMLElement> = {
target:T;
type:string;
}
//Good
type MyEvent
<Type extends string,Target extends HTMLElement = HTMLElement> = {
target:Target;
type:Type;
}
//Bad
type MyEvent
<Target extends HTMLElement = HTMLElement,
Type extends string> = {...}
Polymorphim with interfaces and classes
class MyMap<K,V> {
constructor(initialKey:K,initialValue:V) {
}
get(key:K):V { }
set(key:K,value:V):void { }
static of<K, V>(k: K, v: V): MyMap<K,V> {
// Note that static methods do not have access to their class's
// generic, declare it owns K and V
}
}
Generic from lodash
....
Type-driven development
When you write a TypeScript program, start by defining your functions’ type signa‐ tures—in other words, lead with the types—filling in the implementations later. By sketching out your program out at the type level first, you make sure that everything makes sense at a high level before you get down to your implementations.