058 Вводное видео

Дженерики - это определённая функция с плейсходлдером, в которую мы можем подставить определённый тип. Он позволяет сохранить типовую динамичность функции (как в нативном JS) и сохраняет type safety в проектах. Мы используем дженерики для эффективного переиспользования кода.

059 Пример встроенных generic

Встроенные дженерики в ТС отчётливо показывают нам, какие типы должны возвращать и хранить объекты. Если создать тот же массив чисел мы можем через number[], то уже определить возвращаемый из промиса тип у нас просто так не получится. Чтобы реализовать типизацию у промиса, можно прописать Promise<возвращаемый_тип>

const numArr: Array<number> = [1, 2, 3];  
  
async function promiseFrom() {  
    const promiseA = await new Promise<number>((resolve, reject) => {  
        resolve(1);  
    })  
}

И вот пример создания записной книжки через объект (а не интерфейс), которая принимает в себя ключ(строку) - значение(булеан).

 const recorder: Record<string, boolean> = {  
    drive: true,  
    ride: true  
}

060 Пишем функцию с generic

Дженерики могут нам пригодиться ровно в тех случаях, когда нам нужно реализовать функцию, которая в себя может принять и обработать любой тип данных (либо какие-то определённые) Конкретно в этом случае, мы передаём пока один тип значения - дженерик тут не нужен

function dataMiddleWare(data: string): string {  
    console.log(`Return middleware ${data}`);  
    return data;  
}  
  
const res = dataMiddleWare("str");

Но если у нас появится потребность добавить ещё один тип в качестве передаваемого значения, то нам нужно будет реализовывать сужение под каждый из принимаемых типов

function dataMiddleWare(data: string | number): string | number {  
    console.log(`Return middleware ${data}`);      
if (typeof data === "string") {  
        return data + " string";  
    } else {  
        return data;  
    }  
}  
  
const res = dataMiddleWare("str");

И вот на помощь нам приходят дженерики. Они позволяют сделать некую обобщённую функцию, которая позволит работать с любым передаваемым типом Главный признак таких функций - они могут работать с any

function dataMiddleWare<T>(data: T): T {  
    console.log(`Return middleware ${data}`);  
    return data;  
}  
  
const res = dataMiddleWare("str");

И теперь мы видим, что наша функция универсальна для любых типов данных

Так же хочется отметить, что мы можем валидировать типы данных, получаемых из дженериков Нам нужно получить строку из дженерика - укажем этот тип

Далее нам нужно создать функцию, которая будет принимать в себя массив и возвращать половину от него. Создадим обобщённый дженерик и сталкиваемся с проблемой, что компилятор нам говорит - не у всех передаваемых значений будет значение длины (которое нам нужно от массива) Но тут наша функция не может работать с any данными

function getHalfFromMassive<T>(data: T): T {  
    const lengthHalfedMassive = data.length / 2;  // Error
    return data.slice(0, lengthHalfedMassive);   
}

А вот уже в данном случае, мы сможем обратиться к свойству массива, так как data имеет тип: Массив<любой_тип_данных>. И возвращаем тут тоже массив любого типа данных

function getHalfFromMassive<T>(data: Array<T>): Array<T> {  
    const lengthHalfedMassive = data.length / 2;  
    return data.slice(0, lengthHalfedMassive);  
}
 
getHalfFromMassive<number>([1, 2, 3, 4]);

Таким образом мы можем записать стрелочную функцию

const func = <T>(data: Array<T>): Array<T> => { return data; }

Так же дженерикам можно задать значение по умолчанию

type Constructor = new (...args: any[]) => {};  
type GConstructor<T = {}> = new (...args: any[]) => T;

061 Упражнение - Функция преобразования в строку

Нам нужно реализовать функцию toString, которая будет выводить либо строку, либо undefined (если значение не передано)

function toString<T>(data: T): string | undefined {  
    if (Array.isArray(data)) {  
        return data.toString();  
    }  
  
    switch (typeof data) {  // на каждый тип - своя реализация
        case "string":  
            return data;  
        case "bigint":  
        case "number":  
        case "boolean":  
        case "function":  
            return data.toString();  
        case "object":  
            return JSON.stringify(data);  
        default:  
            return undefined;  
    }  
}  
  
console.log(toString([1, 2, 3]));  
console.log(toString({ a: 123, b: "Logan"}));  
// можно определить самостоятельно передаваемый тип в <>
console.log(toString<boolean>(true)); 

062 Использование в типах

Таким образом мы можем присвоить функцию в другую переменную, описав её тип дженериком

const split: <T>(data: Array<T>) => Array<T> = getHalfFromMassive;

Дженерики можно использовать не только в функциях, но и в рамках описания любого типа объектов в рамках интерфейсов или типов (а так же классов)

interface ILogLine<T> {  // Interface Generic
    name: string,  
    data: T  
}  
  
type LogLineType<T> = {  // Type Generic
    name: string,  
    data: T  
}  
						  // Тип дженерика интерфейса
const logLineInterface: ILogLine<{a: number}> = {  
    name: "Lossy",  
    data: {  
        a: 19  
    }  
}  
						  // Тип дженерика тайпа
const logLineType: LogLineType<{a: number}> = {  
    name: "Lossy",  
    data: {  
        a: 19  
    }  
}

063 Ограничение generic

В ТС есть возможность ограничить получаемые значения дженерика через extends этого дженерика типами, классами, интерфейсами или тайпами

function kmToMiles<T extends Vehicle>(vehicle: T): T { 

И тут мы видим пример, когда мы ограничиваем передаваемые типы в дженерик-функцию, тип которой экстендится от класса Vehicle

class Vehicle {  
    run: number;  
}  
  
class SCV extends Vehicle {  
    capacity: number;  
}  
  
function kmToMiles<T extends Vehicle>(vehicle: T): T { // Расширение  
    vehicle.run = vehicle.run / 0.62;  
    console.log(vehicle);  
    return vehicle;  
}  
  
kmToMiles(new Vehicle());  
kmToMiles(new SCV());  
kmToMiles({run: 143});  
  
kmToMiles({a: 123123}); // Error

Так же extends работает и с интерфейсами

interface Vehicle {  
    run: number;  
}  
  
interface SCV extends Vehicle {  
    capacity: number;  
}  
  
function kmToMiles<T extends Vehicle>(vehicle: T): T { // Расширение  
    vehicle.run = vehicle.run / 0.62;  
    console.log(vehicle);  
    return vehicle;  
}  

Так же дженерики можно экстендить union-типами

function logId(id: number | string) {  
    if (typeof id === 'string') {  
        console.log(id);  
        return id;   
	} else {  
        return id + 302;  
    }    
}
function logId<T extends number | string>(id: T): T {  
    console.log(id);  
    return id;  
}

Но так же мы можем добавлять в код сразу несколько дженериков Например, тут T дженерик, ограниченный юнион-типом и Y дженерик без ограничения

function logId<T extends number | string, Y>(id: T, additionalData: Y): { id: T, data: Y } {  
    console.log(id);  
    return { id, data: additionalData };  
}

064 Упражнение - Функция сортировки id

Нужно написать функцию, которая будет сортировать любые объекты с id по возрастанию и убыванию.

Конкретно тут мы имеем определённые данные. Создаём интерфейс, где говорим, что у нас должно присутствовать свойство id. Далее в функции нужно указать, что мы экстендим наш дженерик интерфейсом, который содержит нужное нам свойство. Аргументом и возвращаемым типом является массив от дженерика. Внутри функции реализована сортировка через switch, который выбирает реализацию из указанной нами (asc-desc)

const data = [  
    {id: 1, name: 'John'},  
    {id: 1, name: 'Lusy'},  
    {id: 1, name: 'Andrew'},  
];  
  
interface ID {  
    id: number  
}  
  
function sort<T extends ID>(data: T[], type: "asc" | "desc" = "asc"): T[] {  
    return data.sort((a, b) => {  
        switch (type) {  
            case "asc":  
                return a.id - b.id;  
            case "desc":  
                return b.id - a.id;  
        }    
    });  
}  
  
console.log(sort(data));  
console.log(sort(data, "desc"));

065 Generic классы

Мы можем так же спокойно задавать дженерики для классов. Они будут определять какие типы данных мы будем передавать в конструктор класса.

class Resp<D, E> {  
    data?: D;  
    error?: E;  
  
    constructor(data?: D, error?: E) {  
        if (data) {  
            this.data = data;  
        }        if (error) {  
            this.error = error;  
        }    
    }  
}  
  
const resp = new Resp<string, number>("data", 0);

Однако если мы не передадим второе значение (опциональное) и не определим типы, то второе значение дженерика будет unknown

Так же нужно сказать, что мы не можем напрямую наследоваться от класса с его дженериками. Мы можем наследоваться от класса и определить дженерики конкретными типами

Так же мы можем указать свои дженерики для наследуемого класса. Однако именам дженериков нельзя совпадать с родительскими

class HTTPResp<F> extends Resp<string, number> {  
    private _code?: F;  
  
    set code(code: F) {  
        this._code = code;  
    }  
}

Дженерики для классов обычно задают, когда у нас свойства зависят от реализации

066 Mixins

Три типа наследования:

  • Через extends
  • Композиция
  • Прямое наследование

Это обычный подход при добавлении нового функционала в классы. Им мы обычно и пользуемся. Конкретно в этом примере такой подход оптимален.

class List {  
    constructor(public items: string[]) {}  
}  
  
type ListType = GConstructor<List>;  
  
class ExtendedListClass extends List {  
    first() {  
        return this.items[0];  
    }  
}

И вот пример миксина. Сразу нужно сказать, что миксины используются редко и обычно используются в подходе DCI. Что из себя представляет миксин? Миксин - это функция, которая возвращает класс, который мы расширили нужным нам классом. Конкретно в нашем примере, TBase (дженерик) мы расширили через ListType и этот TBase определили как тип значения, которое мы передаём в функцию. Аргумент функции - это класс, которым мы будем расширять возвращаемый из функции класс.

type Constructor = new (...args: any[]) => {};  
type GConstructor<T = {}> = new (...args: any[]) => T;  
  
class List {  
    constructor(public items: string[]) {}  
}  
  
type ListType = GConstructor<List>;  
  
class ExtendedListClass extends List {  
    first() {  
        return this.items[0];  
    }  
}  
  // И вот реализация миксина
function ExtendedList<TBase extends ListType>(Base: TBase) {  
    return class ExtendedList extends Base {  
        first() {  
            return this.items[0];  
        }    
    }  
}

Чтобы воспользоваться данной конструкцией, нам нужно сначала присвоить класс какой-то переменной, потом из этой переменной инстанциировать класс

const list = ExtendedList(List);  
const res = new list(["first---", "second"]);  
console.log(res.first()); // first---

Так же главной фишкой такого подхода является то, что мы можем заэкстендить дженерик сразу от нескольких тайпов Связующим звеном для использования является AccordionList - он содержит конструктор List и свойство isOpen (делается это так из-за того, что нам нужно передать класс, который имеет реализацию обоих тайпов которые были сделаны из этих классов)

type Constructor = new (...args: any[]) => {};  
type GConstructor<T = {}> = new (...args: any[]) => T;  // Тайп-Дженерик-Конструктор
  
class List {  // Класс
    constructor(public items: string[]) {}  
}  
  
class Accordion {  // Класс
    isOpen: boolean;  
}  
  
type ListType = GConstructor<List>; // Тайп из класса
type AccordionType = GConstructor<Accordion>;  // Тайп из класса
  
class ExtendedListClass extends List {  
    first() {  
        return this.items[0];  
    }  
}  
  
//                              Собственно, экстенд тайпов:  
function ExtendedList<TBase extends ListType & AccordionType>(Base: TBase) {  
    return class ExtendedList extends Base {  
        first() {  
            this.isOpen = true;  
            return this.items[0];  
        }    }  
}  
 
// Класс, который имеет в себе реализацию обоих классовых тайпов
class AccordionList { // Для связки двух тайпов  
    isOpen: boolean;  
    constructor(public items: string[]) {}  
}  
  
const list = ExtendedList(AccordionList);  
const res = new list(["first", "second"]);  
console.log(res.first() + " " + res.isOpen);

Преймущества миксинов:

  1. Он позволяет перенести функциональность сразу нескольких классов в один
  2. Позволяет примиксовать так же функциональность к исходному классу
  3. Тайпчекинг экстендедов класса

Когда использовать миксины?

Обычно миксины не используются и вместо них применяют композицию, чтобы перенести функциональность из класса в класс. Однако, если нам нужно будет перенести и функциональность из нескольких классов и свойства этих классов, то тут уже нужно будет использовать миксины