We can define that we want to set type of the argument when we are creating the class

// Will say that we should defie type when we are creating the class
class keyValuePairs<K, V> { // K & V are the types 
    constructor(public key: K, public value: V) {}
}

// Type 1: Defining type using <TYPE, TYPE>
let pair = new keyValuePairs<string, string>("1", "1");
// Type 2: Atomatic difinition
let pairs_2 = new keyValuePairs("2", "2");

Generic Functions

Also we can create functions with generic types inside or outside of a class

class ArrayUtils {
    static wrapInArray<S>(value: S) {
        return [value];
    }
}

// A functional (outside of the class)
function wrapInArray<T>(value: T) {
    return [value];
}
// From The function
const arr = wrapInArray("1");
// In the class
const arrUtil = ArrayUtils.wrapInArray(1);

Generic Interfaces

We can define that we have an interface that have generic parameter (Result) and then we create a function that say it need a generic ( Interface Type For the Result(Interface) ). the we can access to the parent interface (Result) and give the child interfaces as property.

// Define interface with generic parameter
interface Result<T> {
    data: T | null;
    error: string | null;
}

// Define that fetch need a type parameter for the result
// And return result of the (T)
function fetch<T>(url: string): Result<T> {
    return { data: null, error: null };
}

interface User {
    username: string;
}

interface Product {
    title: string;
}

const resultUser = fetch<User>("url");
resultUser.data?.username; // We have access to the username from data (User interface)

const resultProduct = fetch<Product>("url");
resultProduct.data?.title; // We have access to the Product interface

Generics Limits

We can define limits for the type of the generics and force them to accept exact type or teypeS that we want from them.

// Only accept number & string types as argument - extends from primitives
function logNumberString<T extends number | string>(value: T): T {
    return value;
}
// Only accept (object) with (name) key - Extends from object
function logObjects<T extends { name: string }>(value: T): T {
    return value;
}

interface Person {
    name: string;
}
// Only accept (Person) interface as the type - extends from interface
function logInterface<T extends Person>(value: T): T {
    return value;
}

// Only Accept Man or People classes as type - extends from Class
class People {
    constructor(public name: string) {}
}
class Man extends People {
    constructor(public name: string) {
        super(name);
    }
}
function logClass<T extends Man>(value: T): T {
    return value;
}

logNumberString(0);
logObjects({ name: "Nima" });
logClass(new Man("Name"));

Extending Generic Classes & Inheritance

We can extend generic classes like these.

interface Product {
    name: string;
    price: number;
}

class Store<T> {
    protected _objects: T[] = [];

    add(obj: T): void {
        this._objects.push(obj);
    }

    getObject() {
        return this._objects;
    }
}

class CompressibleStore<T> extends Store<T> {
    compress() {}
}

class SearchableStore<T extends { name: string }> extends Store<T> {
    find(name: string): T | undefined {
        return this._objects.find((obj) => obj.name === name);
    }
}

class ProductStore extends Store<Product> {
    filterByCategory(category: string, price: number): Product[] {
        return [{ name: category, price: price }];
    }
}

let compressibleStore = new CompressibleStore<Product>();
let storeClass = new Store();
let productStore = new ProductStore();
let searchAble = new SearchableStore();

// Access to child
compressibleStore.compress();

// Adding (a1) to the the (_objects)
storeClass.add("a");
console.log("objects => ", storeClass.getObject());

console.log("return given data => ", productStore.filterByCategory("a", 500));
console.log(searchAble.find("a"));

Keyof Operator

using keyof operator we can say that we want to get argument as the key of the objects. Or simply accept just object keys

interface Product {
    name: string;
    price: number;
}

class Store<T> {
    protected _objects: T[] = [];

    add(obj: T): void {
        this._objects.push(obj);
    }

    getObject() {
        return this._objects;
    }

    // (keyof T) will say argument should be match with object keys
    find(property: keyof T, value: unknown): T | undefined {
        return this._objects.find((obj) => obj[property] === value);
    }
}

let store = new Store<Product>();
store.add({ name: "bmw", price: 20 });
store.find("name", "bmw");
// store.find("brand", 5); // will not work because key not exist

Type Mapping

Using type mapping we can define that we want different behaviors from an interface. so we can change type of the properties of the object.

interface Product {
    name: string;
    price: number;
}

// This will create anything in the product interface readonly
type ReadOnlyProducts = {
    readonly // (K) is the key => it is like a for loop (This just work for Product interface)
    [K in keyof Product]: Product[K];
};

let product: ReadOnlyProducts = {
    name: "a",
    price: 5,
};
// product.name = "5"; // here we can not change the value of the name will get an error

/******** Next level Readonly *********/
type Optional<T> = {
    // using (?) made it optional
    [K in keyof T]?: T[K]; // made every thing here optional
};

type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

let optional: Optional<Product> = {
    name: "name",
    // price is optional here
};

let nullable: Nullable<Product> = {
    name: null,
    price: null,
};

Leave a Reply

Required fields are marked *