Домашнє завдання #18: Типізація функцій та змінних
Поглянь на цей код. У ДЗ #17 такий баг міг спокійно доїхати до продакшену:
const user = { name: "Олексій", email: "alex@blog.com" };
console.log(user.emial.toUpperCase()); // TypeError — але дізнаєшся про це в рантаймі
TypeScript зловив би його ще в редакторі: Property 'emial' does not exist on type 'User'. Did you mean 'email'? Саме заради цього ми й переходимо на TS. Тобі дано набір JavaScript-функцій без типів — перепиши їх на TypeScript з повною типізацією.
Що ти вже маєш: у ДЗ #17 ти побудував клас User для блог-платформи. Сьогодні та сама модель стає типом User — ті ж name, email, age, плюс id та status. Одна модель, що еволюціонує разом з тобою.
Файлова структура
Два файли: hw-18/src/index.ts (типи та всі 8 функцій) і мінімальний, але суворий hw-18/tsconfig.json:
{
"compilerOptions": { "target": "ES2020", "strict": true, "noEmit": true },
"include": ["src"]
}
Перевірка одна: npx tsc --noEmit має проходити без жодної помилки.
Завдання
1. Type aliases
Створи типи, які використовуватимуться далі:
| Тип | Що описує |
|---|---|
ID | Ідентифікатор — рядок або число (union) |
UserStatus | Статус користувача — рівно три варіанти: "active", "inactive", "banned" (literal union) |
User | Об'єкт користувача: id (тип ID), name, email, age, status (тип UserStatus) |
2. processArray
// Оригінал (JS):
function processArray(arr, action) {
if (action === "sum") return arr.reduce((acc, n) => acc + n, 0);
if (action === "average") return arr.reduce((acc, n) => acc + n, 0) / arr.length;
if (action === "max") return Math.max(...arr);
if (action === "min") return Math.min(...arr);
return null;
}
Вимоги: arr — масив чисел; action — literal type з чотирьох допустимих значень; return type — number | null.
3. filterByType
// Оригінал (JS):
function filterByType(arr, typeName) {
return arr.filter(item => typeof item === typeName);
}
Вимоги: arr — масив (string | number | boolean)[]; typeName — literal type "string" | "number" | "boolean"; return type — той самий union-масив.
4. calculateStats
// Оригінал (JS):
function calculateStats(numbers) {
if (numbers.length === 0) {
return { count: 0, sum: 0, average: 0, min: 0, max: 0 };
}
const sum = numbers.reduce((acc, n) => acc + n, 0);
return {
count: numbers.length, sum, average: sum / numbers.length,
min: Math.min(...numbers), max: Math.max(...numbers),
};
}
Вимоги: створи type alias Stats для об'єкта-результату; numbers — масив чисел; return type — Stats.
5. formatUser
// Оригінал (JS):
function formatUser(user, format) {
if (format === "short") return user.name;
if (format === "medium") return `${user.name} (${user.email})`;
if (format === "full") return `${user.name} (${user.email}), вік: ${user.age}, статус: ${user.status}`;
return user.name;
}
Вимоги: user — тип User; format — literal type "short" | "medium" | "full"; return type — string.
6. parseConfig
// Оригінал (JS):
function parseConfig(raw) {
const config = { host: "localhost", port: 3000, debug: false };
if (raw.host) config.host = raw.host;
if (raw.port) config.port = raw.port;
if (raw.debug !== undefined) config.debug = raw.debug;
return config;
}
Вимоги: type alias Config — повний конфіг (всі поля обов'язкові); type alias PartialConfig — вхідний параметр (всі поля optional); return type — Config.
7. groupBy
// Оригінал (JS):
function groupBy(arr, key) {
return arr.reduce((groups, item) => {
const value = item[key];
if (!groups[value]) groups[value] = [];
groups[value].push(item);
return groups;
}, {});
}
Вимоги: arr — User[]; key — ключ типу User (використай keyof); return type — Record<string, User[]>.
8. createValidator
// Оригінал (JS):
function createValidator(rules) {
return function validate(value) {
const errors = [];
for (const rule of rules) {
const error = rule(value);
if (error) errors.push(error);
}
return errors;
};
}
Вимоги: type alias ValidationRule — функція (value: string) => string | null; rules — ValidationRule[]; return — функція (value: string) => string[].
Приклади вводу/виводу
const user: User = { id: 1, name: "Олексій", email: "alex@blog.com", age: 25, status: "active" };
processArray([1, 2, 3, 4, 5], "sum"); // 15
processArray([10, 20, 30], "average"); // 20
processArray([], "avg"); // ПОМИЛКА КОМПІЛЯЦІЇ: "avg" не входить у literal type
calculateStats([10, 20, 30, 40, 50]); // { count: 5, sum: 150, average: 30, min: 10, max: 50 }
formatUser(user, "full"); // "Олексій (alex@blog.com), вік: 25, статус: active"
groupBy([user], "salary"); // ПОМИЛКА КОМПІЛЯЦІЇ: "salary" — не ключ User
const validateEmail = createValidator([
(v) => (v.length === 0 ? "Email обов'язковий" : null),
(v) => (!v.includes("@") ? "Email має містити @" : null),
]);
validateEmail("test"); // ["Email має містити @"]
validateEmail("test@ok.com"); // []
Бонус
- Зроби
groupBygeneric-функцією, яка працює з будь-якими об'єктами (не тількиUser) - Додай overloading для
formatUser, де кожен формат повертає більш конкретний тип - Створи type guard
isUser(value: unknown): value is Userдля валідації об'єктів з API
Підказки
Рівень 1: напрямок
Йди по порядку: спочатку всі type aliases (без них не типізуєш функції 5 і 7), потім функції одна за одною. Після кожної — запускай npx tsc --noEmit. Помилки компілятора — це не покарання, а безкоштовне код-рев'ю: читай їх уважно, там зазвичай написано, що саме виправити.
Рівень 2: синтаксис
Union: type ID = string | number. Literal union: type UserStatus = "active" | "inactive" | "banned". Тип об'єкта: type User = { id: ID; name: string; ... }. Анотація функції: function f(arr: number[], action: "sum" | "max"): number | null. Optional-поле: host?: string. Ключі типу: key: keyof User. Тип-функція: type ValidationRule = (value: string) => string | null.
Рівень 3: складні місця
У groupBy значення item[key] має тип «значення будь-якого поля User» — щоб використати його як ключ об'єкта, приведи до рядка: String(item[key]). Акумулятор reduce типізуй явно: arr.reduce<Record<string, User[]>>((groups, item) => { ... }, {}). У parseConfig тип PartialConfig можна написати вручну з ? для кожного поля — а можеш підгледіти utility type Partial<Config>, який ти офіційно вивчиш у наступному уроці.
Як здати
- Робота — у твоєму репозиторії курсу
my-web-course, у папціhw-18/ - У папці —
tsconfig.jsonтаREADME.md: 2-3 речення що зроблено + команда перевірки npx tsc --noEmitпроходить без помилок — це перше, що перевірить чекер- Мінімум 3 коміти з осмисленими повідомленнями — наприклад: type aliases, функції 2–5, функції 6–8
- Здай посиланням на папку
hw-18у репозиторії
Критерії оцінювання
| Критерій | Тип | Бали |
|---|---|---|
| Type aliases: ID, UserStatus, User, Stats, Config, PartialConfig | [код] | 15 |
| processArray: literal type для action, return number або null | [код] | 10 |
| filterByType: union types у параметрі та return | [код] | 10 |
| calculateStats: Stats type alias, коректна обробка порожнього масиву | [код] | 10 |
| formatUser: тип User, literal type для format | [код] | 10 |
| parseConfig: PartialConfig на вході, Config на виході | [код] | 10 |
| groupBy: keyof User, Record у return type | [код] | 15 |
| createValidator: type alias для функції-правила, типізований return | [код] | 10 |
| npx tsc --noEmit без помилок, strict: true, жодного any | [код] | 5 |
| Якість коду | [код] | 5 |
| Разом | 100 | |
| Бонус: generic groupBy / overloading / type guard | [код] | +20 |
Що далі
Тип User — це лише початок. У ДЗ #19 ти опишеш інтерфейсами всю доменну модель свого продукту, а в ДЗ #20 ці типи запрацюють у справжньому застосунку. Не видаляй код!