Chupurnov Valeriy
Chupurnov Valeriy
Front End Engineer

TypeScript правильная перегрузка функций

TypeScript правильная перегрузка функций

Если вы работали с TS, то вам должна быть знакома такая конструкция:

function double(a: string): string;
function double(a: number): number;
function double(a: any) {
    return a + a;
}

Это механизм оверайда или перегрузки объявления типа функций. Таким же образом можно перегружать и методы классов. Это очень удобно, мы точно сообщаем TS что если подать в функцию number, то она вернет number.

Так что же не так с этим примером?

Все еще читаю книгу Эффективный Typescript, и там в главе 6 об этом рассказано. А я делюсь с вами и за одним закрепляю материал в своей голове.

Для начала небольшой ликбез:

Зачем вообще нужна перегрузка?

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

К примеру eventEmmiter в моем редакторе Jodit поддерживает такой вызов навешивания обработчиков событий:

class EventEmmiter {
    on(eventName: string, handler: Function): this;
    on(target: HTMLElement, eventName: string, handler: Function): this;
    on(targetOrEventName: string | HTMLElement, eventNameOrFunction: string | Function, handler?: Function): this {
        const eventName = typeof eventNameOrFunction === 'string' ? eventNameOrFunction : targetOrEventName;
        const target = typeof targetOrEventName !== 'string' ? targetOrEventName : window;
        const handler = typeof eventNameOrFunction === 'function' ? eventNameOrFunction : handler;
        
        target.addEventListener(eventName, handler);
        return this;
    }
}

Такой код позволяет писать интересные конструкции:

const emmiter = new EventEmmiter();
emmiter
    .on('click', (e) => {
        alert('Click on Window');
    })
    .on(document.body, 'click', (e) => {
        alert('Click on document');
    })
    .on(document.querySelector('#root'), (e) => {
        alert('Click on the root');
    })

Так, с перегрузкой разобрались.

Так что же не так с первым примером?

Это объявление плохо себя ведет при таком использовании:

const a: string | number = someFunc();
double(a); // Тут будет ошибка

Наша реализация типов умеет принимать типы отдельно, но не их объединение. В книге приводится один из вариантов решения. Так называемые дженерики:

function double<T>(a: T): T;

Я с удивлением узнал, что такой код является устаревшим и так писать не стоит. Казалось бы, идеально? Какой тип пришел, такой и уйдет. Нет, не идеально.

const v = double('a');
// v имеет тип не string, а более узкий 'a' 

На помощь придут тернарные выражения из мира TypeScript или условные типы:

function double<T extends string | number>(a: T): T extends number ? number : string;

Теперь все работает, как и задумано.

Всем добра!