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'.
💡
object doesn't tell you anything about the shape of an object.So each time we try to access a property on an object in array, TS will throws an error because we didn't tell it what specific shape the object has

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

💡
T is declared as part of the Filter's type(and not part of the signature call), Typescript will bind T when you declare a function of type Filter
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

💡
Many functions in the JavaScript standard library are generic, especially those on Array’s prototype.

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;
}
💡
Note that this is the only valid place to declare a generic type in a type alias: right after the type alias’s name, before its assignment (=).
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;
    }
}
💡
mapNode can returns TreeNode and its subtype of TreeNode. Much more accurate if compare that we only return TreeNode

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;
}
💡
Default generic type parameter has to appear after generic type without default (just like in function
//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

💡
A style of programming where you sketch out type signa‐ tures first, and fill in values later.

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.