Generic-типы

Базовый пример

const arrayOfNumbers: Array<number> = [1, 2, 3, 4]

function reverse<T>(array: T[]): T[] {
    return array.reverse()
}

Generic и Promise

Использование generic в промисах:

// Через Generic тип указываем с каким типом работает Promise
const promise = new Promise<string>(resolve => {
    setTimeout(() => {
        resolve('Promise resolved')
    }, 2000)
})

// data имеет тип string теперь
promise.then(data => {
    console.log(data)
})

/*
Что здесь происходит:
1. создается callback-функция: data => {console.log(data)}
2. resolve инициализируется значением callback-функции через вызов у promise метода then
3. Когда promise запущен, внутри него будет вызван resolve
*/

Generic типы для создания более гибких функций и проверки самого себя на этапе разработки

Использование для получения доступа к информации о полях

function mergeObjects<T, R>(a: T, b: R): T & R {
    return Object.assign({}, a, b)
}

const merged = mergeObjects({name: 'Test'}, {age: 20})

console.log(merged.name)  // Без указания типа через Generic, мы бы не могли получить доступ к полям созданного объекта


// ========

/*
Здесь есть проблема: Object.assign ожидает объекты на вход, а не простые типы
Сейчас мы можем передать и простой тип: mergeObjects('abc', 'def')
И на выходе гавно получим

Вот так можно ввести ограничение (constraints) на тип в generic:
*/
function test<T extends object>(a: T) {
    // ...
}


// =====
// Другой пример
// Описание возвращаемого значения:
interface ILength {
    length: number
}

function withCount<T extends ILength>(value: T): {value: T, count: string} {
    return {
        value,
        count: `Count: ${value.length}`
    }
}



// ========
// Еще пример встроенной проверки: 
//    первый тип — объект
//    второй тип — указываем, что это тип ключа первого объекта! (через keyof оператор)
function getObjectValue<T extends object, R extends keyof T>(obj: T, key: R) {
    return obj[key]
}

const person = {
    test: 123
}
console.log(getObjectValue(person, 'test'))
console.log(getObjectValue(person, 'notexist')) // Компилятор укажет ошибку

Generic типы в классах

class Collection<T extends number | script | boolean> {
    
    constructor(private _items: T[] = []) {}
    
    add(item: T) {
        this._items.push(item)
    }
    
    remove(item: T) {
        this._items = this._items.filter(i => i !== item)
    }
    
    get items(): T[] {
        return this._items
    }
}

const strings = new Collection(['a', 'b', 'c'])
strings.add('d')
strings.remove('a')

Partial: Generic типы и временные объявления

interface Car {
    model: string
}

/*
  Здесь будет ошибка: car должно иметь поле model, а оно инициализируется пустым объектом
*/
function test(model: string): Car {
    const car: Car = {}
    
    if (model.length > 0) {
        car.model = model
    }
    
    return car
}

/*
  Поправим это с помощью отложенной инициализации
*/
function test2(model: string): Car {
    // Мы говорим компилятору: "Да, мы знаем, что у объекта нет поля model, 
    // оно будет проинициализировано позднее"
    
    const car: Partial<Car> = {}  
    
    if (model.length > 0) {
        car.model = model
    }
    
    // А здесь делаем привидение типа от Partial<Car> к Car
    return car as Car  
}

Readonly инструмент

Этот инструмент позволяет блокировать изменение значений полей

const cars: Readonly<Array<string>> = ['Ford', 'Audi']
cars.shift()  // Здесь компилятор выдаст ошибку: изменять значения внутри нельзя!

То же самое можно делать и с объектами.

Last updated