На предыдущих уроках мы с вами разобрались с основами типов, интерфейсами и типами для объектов и массивов. Мы стали чувствовать себя гораздо увереннее, ведь наш код теперь не только понятнее, но и надежнее благодаря строгой типизации.
Но сегодня мы переходим к одному из самых важных и фундаментальных аспектов TypeScript, типизации функций. Функции это сердце любого приложения на JavaScript и TypeScript дарит им суперспособности, которые делают нашу разработку по-настоящему профессиональной. Мы научимся точно описывать, какие данные функция ожидает на входе и что именно она возвращает на выходе. Это как заключать контракт, функция обязуется работать только с указанными типами, а компилятор обязуется следить, чтобы этот контракт не нарушался.
Аннотирование параметров функции
В ванильном JavaScript мы можем передать в функцию любые аргументы в любом количестве. Если мы передадим меньше, чем ожидает функция, параметры станут undefined. Если передадим больше, лишние просто проигнорируются. Это может быть источником множества ошибок, которые сложно отловить на раннем этапе.
TypeScript решает эту проблему, позволяя нам явно указывать типы для каждого параметра функции. Синтаксис очень простой: после имени параметра ставим двоеточие и указываем его тип.
// JavaScript: никакой проверки типов на этапе написания кода function greetJS(name) { return `Hello, ${name}!`; } console.log(greetJS('Maxim')); // OK console.log(greetJS(42)); // Также "OK", но логически неверно // TypeScript: строгая проверка типов параметров function greetTS(name: string) { return `Hello, ${name}!`; } console.log(greetTS('Maxim')); // OK console.log(greetTS(42)); // Ошибка компиляции: Argument of type 'number' is not assignable to parameter of type 'string'.
В этом примере мы объявили функцию greetTS, которая имеет один параметр name типа string. Теперь, если мы попытаемся передать число (как в последней строке), TypeScript немедленно выдаст ошибку на этапе компиляции, не дав нам допустить эту оплошность в рантайме.
Мы можем типизировать любое количество параметров.
function introduceYourself( name: string, age: number, isStudent: boolean ): void { console.log( `My name is ${name}. I am ${age} years old. Student status: ${isStudent}.` ); } introduceYourself('Anna', 23, true); // OK introduceYourself('Bob', 'thirty', false); // Ошибка: Argument of type 'string' is not assignable to parameter of type 'number'.
Обратите внимание, как TypeScript четко указывает, какой именно аргумент не соответствует ожидаемому типу. Это невероятно упрощает отладку.
Необязательные параметры
В жизни не всегда нужно передавать все возможные аргументы. Для таких случаев TypeScript предоставляет синтаксис для необязательных параметров. Чтобы пометить параметр как необязательный, нужно после его имени поставить знак вопроса ?.
function buildAddress( city: string, street: string, house: number, apartment?: number // Необязательный параметр ): string { let address = `${city}, ${street}, ${house}`; if (apartment !== undefined) { address += `, apt. ${apartment}`; } return address; } console.log(buildAddress('Moscow', 'Arbat', 10)); // "Moscow, Arbat, 10" console.log(buildAddress('SPb', 'Nevsky', 5, 12)); // "SPb, Nevsky, 5, apt. 12" console.log(buildAddress('Moscow', 'Tverskaya', 15, undefined)); // Можно и так, но лучше просто не передавать
Важный момент: необязательные параметры всегда должны идти после обязательных. Если вы попытаетесь поставить необязательный параметр перед обязательным, TypeScript выдаст ошибку: A required parameter cannot follow an optional parameter.
Параметры по умолчанию
Синтаксис параметров по умолчанию пришел в TypeScript из ES6 и TypeScript отлично с ним работает. Более того, TypeScript может автоматически вывести тип параметра с default value на основе переданного значения.
function createGreeting(message: string, userName: string = 'Guest'): string { return `${message}, ${userName}!`; } console.log(createGreeting('Welcome')); // "Welcome, Guest!" console.log(createGreeting('Hi', 'Maxim')); // "Hi, Maxim!"
В этом примере параметр userName автоматически считается типом string, потому что его значение по умолчанию строка 'Guest'. Явно указывать : string в данном случае не обязательно, но часто делается для лучшей читаемости кода.
Аннотирование возвращаемого значения
Теперь давайте поговорим о том, что функция возвращает. Указывать тип возвращаемого значения не менее важно, чем типы параметров. Это позволяет убедиться, что функция действительно возвращает то, что от нее ожидают и что результат ее работы используется корректно.
Тип возвращаемого значения указывается после списка параметров, перед открывающей фигурной скобкой.
function add(a: number, b: number): number { return a + b; } const result: number = add(5, 3); // OK const errorResult: string = add(5, 3); // Ошибка: Type 'number' is not assignable to type 'string'.
В этом примере мы явно сказали, что функция add возвращает number. Это не позволяет нам записать результат в переменную типа string.
А что если наша функция ничего не возвращает? Для этого в TypeScript существует специальный тип void. Он как раз и означает отсутствие возвращаемого значения.
function logError(message: string): void { console.error(`Error: ${message}`); // Здесь нет return, поэтому функция возвращает undefined }
Важно понимать, что в JavaScript функция без return возвращает undefined. void в TypeScript это не то же самое, что undefined. void это специальная пометка, говорящая о том, что возвращаемое значение функции не должно использоваться.
Если же функция вообще никогда не завершает свое выполнение (например, бросает исключение или содержит бесконечный цикл), для этого предназначен тип never.
function throwError(message: string): never { throw new Error(message); } function infiniteLoop(): never { while (true) { // do something... } }
TypeScript часто бывает достаточно умен, чтобы самостоятельно вывести тип возвращаемого значения. Например, в функции add из примера выше мы могли бы не писать : number и TS увидел бы, что мы складываем два числа и вернул бы тип number.
// TypeScript автоматически вывел тип возвращаемого значения как number function add(a: number, b: number) { return a + b; }
Однако я настоятельно рекомендую всегда явно указывать тип возвращаемого значения. Это сделает ваш код более читаемым, поможет избежать случайных ошибок (например, если вы в процессе рефакторинга поменяете логику функции и она начнет возвращать другой тип) и будет служить отличной документацией.
Разница между function и стрелочными функциями
В ES6 появился новый лаконичный синтаксис для функций, стрелочные функции. Они не только короче записываются, но и имеют важное отличие в поведении ключевого слова this. TypeScript применяет все правила типизации одинаково как к обычным функциям, объявленным с помощью function, так и к стрелочным функциям.
Синтаксис типизации
Принцип аннотирования параметров и возвращаемого значения абсолютно идентичен.
Обычная функция:
function multiply(a: number, b: number): number { return a * b; }
Стрелочная функция:
const multiply = (a: number, b: number): number => { return a * b; };
Для лаконичных стрелочных функций, которые сразу возвращают выражение, тип возвращаемого значения также прекрасно выводится автоматически, но его можно и нужно указывать.
// Тип возвращаемого значения (number) выведен автоматически. const multiply = (a: number, b: number) => a * b; // Но лучше указать явно. Это особенно важно в цепочках колбэков. const multiply = (a: number, b: number): number => a * b;
Контекст this
Главное различие между двумя формами объявления функций, это работа с this. В обычных функциях this определяется в момент вызова функции. В стрелочных функциях this привязывается лексически, то есть он берется из окружающего контекста.
TypeScript помогает нам отслеживать ошибки, связанные с this. Рассмотрим классическую проблему.
class Person { name: string = 'Maxim'; // В обычной функции this привязан к контексту вызова sayNameRegular() { console.log(`Regular: My name is ${this.name}`); } // В стрелочной функции this привязан лексически (к контексту класса) sayNameArrow = () => { console.log(`Arrow: My name is ${this.name}`); }; } const person = new Person(); // Прямой вызов работает одинаково. person.sayNameRegular(); // "Regular: My name is Maxim" person.sayNameArrow(); // "Arrow: My name is Maxim" // А теперь посмотрим, что произойдет при передаче методов в колбэк (например, в setTimeout) const regularFunc = person.sayNameRegular; const arrowFunc = person.sayNameArrow; // Потеря контекста! this.name будет undefined (или window.name в браузере). regularFunc(); // Ошибка (в strict mode): Cannot read property 'name' of undefined // Контекст сохранен, так как стрелочная функция захватила this из класса. arrowFunc(); // "Arrow: My name is Maxim"
TypeScript умеет проверять использование this в функциях. Если вы укажете флаг --noImplicitThis в настройках компилятора (что highly recommended), TypeScript будет ругаться, если он не может определить тип this.
Чтобы явно указать тип this в обычной функции (если вы ждете определенный контекст), это можно сделать через первый псевдо-параметр.
interface Button { value: string; click(this: Button): void; } const myButton: Button = { value: 'Submit', click: function() { console.log(this.value); // TypeScript знает, что this имеет тип Button } }; const clickHandler = myButton.click; clickHandler(); // Ошибка компиляции: The 'this' context of type 'void' is not assignable to method's 'this' of type 'Button'.
В этом примере мы явно сказали функции click, что ее this должен быть типа Button. TypeScanner поймает ошибку, когда мы попытаемся вызвать эту функцию без правильного контекста.
Итог по выбору синтаксиса:
-
Используйте стрелочные функции для колбэков и методов классов, где важно сохранить лексический контекст
this. -
Используйте обычные функции
functionдля методов объектов и классов, где вы хотите, чтобыthisдинамически определялся или для функций-конструкторов. -
TypeScript обеспечит типизацию для обоих вариантов.
Перегрузка функций
В TypeScript существует концепция, называемая «перегрузка функций» (function overloading). Она позволяет одной функции иметь несколько сигнатур (объявлений) с разными параметрами и разными возвращаемыми типами.
Это очень полезно, когда одна функция может быть вызвана с разным количеством аргументов или аргументами разных типов и в зависимости от этого возвращает разные результаты.
Синтаксис перегрузки выглядит так: мы пишем несколько объявлений функции (только сигнатуру: параметры и возвращаемый тип), а затем одну реализацию с общей сигнатурой.
// Сигнатуры перегрузки function getUsersData(id: number): User; function getUsersData(id: number[]): User[]; function getUsersData(role: string): User[]; // Общая реализация function getUsersData(param: number | number[] | string): User | User[] | undefined { if (typeof param === 'number') { // Ищем одного пользователя по id и возвращаем User return db.findUser(param); } else if (Array.isArray(param)) { // Ищем нескольких пользователей по массиву id и возвращаем User[] return db.findUsers(param); } else if (typeof param === 'string') { // Ищем пользователей по роли и возвращаем User[] return db.findUsersByRole(param); } // Технически, сюда мы никогда не попадем из-за типов, но на всякий случай return undefined; } // Использование const user = getUsersData(1); // Тип: User const usersById = getUsersData([1, 2, 3]); // Тип: User[] const usersByRole = getUsersData('admin'); // Тип: User[]
Обратите внимание, как TypeScript теперь точно знает тип возвращаемого значения в зависимости от типа переданного аргумента. Без перегрузок тип возвращаемого значения был бы общим User | User[] | undefined и нам пришлось бы постоянно делать сужение типов.
Реализация функции должна быть совместима со всеми объявленными сигнатурами. Это самая сложная часть. Часто внутри используется сужение типов и проверки, чтобы решить, какую именно логику выполнить.
Перегрузки мощный инструмент, но не злоупотребляйте ими. Если функция делает слишком много разных вещей в зависимости от аргументов, возможно, стоит разделить ее на несколько более простых функций.
Практические примеры и задачи
Давайте закрепим теорию практикой. Вот несколько примеров и задач для самостоятельного решения.
Пример 1: Функция-валидатор
Напишем функцию, которая проверяет, является ли переданное значение валидным email-адресом (упрощенная проверка).
function isValidEmail(email: string): boolean { // Простейшая проверка на наличие символа @ return email.includes('@'); } console.log(isValidEmail('hello@world.com')); // true console.log(isValidEmail('invalid-email')); // false // console.log(isValidEmail(123)); // Ошибка компиляции
Пример 2: Функция с объектом-параметром
Часто параметров много и их удобно передавать как один объект. TypeScript отлично с этим справляется.
interface CreateUserParams { username: string; email: string; age?: number; isActive?: boolean; } function createUser(params: CreateUserParams): void { // Логика создания пользователя console.log(`Creating user ${params.username} with email ${params.email}`); } createUser({ username: 'maxim', email: 'max@example.com' }); // OK createUser({ username: 'anna', email: 'anna@example.com', age: 25, isActive: true }); // OK // createUser({ username: 'bob' }); // Ошибка: не хватает обязательного поля email
Задача 1: Типизация функции вычисления площади
Напишите функцию calculateArea, которая:
-
Принимает два параметра:
shape(строка, может быть'rectangle'или'circle') иsize(объект). -
Для прямоугольника (
'rectangle') объектsizeдолжен содержатьwidthиheight(оба числа). -
Для круга (
'circle') объектsizeдолжен содержатьradius(число). -
Функция должна возвращать число (площадь фигуры).
Решение:
// Ваш код здесь // Подсказка: используйте объединение типов для размера // Ответ: function calculateArea( shape: 'rectangle' | 'circle', size: { width: number; height: number } | { radius: number } ): number { if (shape === 'rectangle') { // TypeScript здесь сузит тип size до { width: number; height: number } const { width, height } = size as { width: number; height: number }; // Можно без утверждения, если проверять 'width' in size return width * height; } else { const { radius } = size as { radius: number }; return Math.PI * radius * radius; } } console.log(calculateArea('rectangle', { width: 10, height: 5 })); // 50 console.log(calculateArea('circle', { radius: 3 })); // ~28.27
Бонус: попробуйте решить эту задачу с помощью перегрузки функций.
Задача 2: Функция-каррирование с типизацией
Каррирование это преобразование функции от многих аргументов в набор функций, каждая из которых принимает один аргумент. Напишите каррированную версию функции сложения.
// Обычная функция function add(a: number, b: number): number { return a + b; } // Каррированная функция const curriedAdd = (a: number) => (b: number): number => { return a + b; }; // Использование const addFive = curriedAdd(5); const result = addFive(3); // 8 console.log(result); // Проверка типов // const error = addFive('hello'); // Ошибка компиляции
Заключение
Мы научились аннотировать параметры (включая необязательные и со значениями по умолчанию) и возвращаемые значения. Мы разобрались, как TypeScript работает с разными видами функций, обычными и стрелочными и в чем их ключевое отличие с точки зрения контекста this. Мы даже заглянули в мощную и немного магическую тему перегрузки функций.
В следующих уроках мы изучимв работу с асинхронным кодом, типизацию промисов и async/await. Это будет не менее интересно!
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


