Вивчай
Домашнє завдання #18 · Type annotations, type aliases, union/literal types, keyof
100 балів+20 бонусintermediate

Домашнє завдання #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;
  }, {});
}

Вимоги: arrUser[]; 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; rulesValidationRule[]; 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"); // []

Бонус

  • Зроби groupBy generic-функцією, яка працює з будь-якими об'єктами (не тільки 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>, який ти офіційно вивчиш у наступному уроці.

Як здати

  1. Робота — у твоєму репозиторії курсу my-web-course, у папці hw-18/
  2. У папці — tsconfig.json та README.md: 2-3 речення що зроблено + команда перевірки
  3. npx tsc --noEmit проходить без помилок — це перше, що перевірить чекер
  4. Мінімум 3 коміти з осмисленими повідомленнями — наприклад: type aliases, функції 2–5, функції 6–8
  5. Здай посиланням на папку 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 ці типи запрацюють у справжньому застосунку. Не видаляй код!