Перейти к основному содержимому

Простые приложения на TypeScript

Разработчику

Дальше: TypeScript и React · TypeScript и Node.js · Первая программа · Асинхронность


Теория из 10.md и 4.md закрепляется тремя мини-проектами: консоль, браузерная форма, клиент REST API. В каждом — один доменный тип, строгие функции и явные ветки для пустых данных и ошибок без any.

Маршрут: Первая программапрактикумTypeScript и React или TypeScript и Node.js.

Запросы к API в лаборатории — curl / fetch.


Что общее для всех трёх проектов

ПравилоНазначение
Один type / interface на сущностьединый контракт
const по умолчаниюменьше мутаций
unknown + guard для внешних данныхчестная модель
Union для состояний UI/APIвсе ветки видны компилятору — 17.md
tsc --noEmit перед сдачейкак в CI — 3.md

Консольное приложение: Todo CLI

Цель

Массив задач в памяти: добавить, завершить, список. Без БД — фокус на типах и find.

src/todo.ts:

export type Todo = {
id: number;
title: string;
done: boolean;
};

const todos: Todo[] = [];

export function addTodo(title: string): Todo {
const todo: Todo = {
id: Date.now(),
title: title.trim(),
done: false,
};
if (!todo.title) throw new Error("title required");
todos.push(todo);
return todo;
}

export function completeTodo(id: number): Result<"ok" | "not-found"> {
const todo = todos.find((t) => t.id === id);
if (!todo) return { ok: false, error: "not-found" };
todo.done = true;
return { ok: true, value: "ok" };
}

type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };

export function listTodos(): readonly Todo[] {
return todos;
}

src/index.ts:

import { addTodo, completeTodo, listTodos } from "./todo.js";

addTodo("Изучить TypeScript");
addTodo("Запустить tsc");
completeTodo(listTodos()[0]?.id ?? 0);
console.log(listTodos());

Разбор:

  • findTodo | undefined — обработка not-found через Result27.md.
  • Импорт с .js при NodeNext9.md.

Расширения

  • CLI-аргументы через process.argv и парсинг команд.
  • Сохранение в todos.json с readFile / writeFile22.md.

Веб-форма: регистрация пользователя

Цель

Собрать RegistrationForm, валидировать до fetch, показать ошибки.

type RegistrationForm = {
email: string;
password: string;
};

type FieldErrors = Partial<Record<keyof RegistrationForm, string>>;

function validate(form: RegistrationForm): FieldErrors {
const errors: FieldErrors = {};
if (!form.email.includes("@")) errors.email = "Некорректный email";
if (form.password.length < 8) errors.password = "Минимум 8 символов";
return errors;
}

function readForm(formEl: HTMLFormElement): RegistrationForm {
const data = new FormData(formEl);
return {
email: String(data.get("email") ?? ""),
password: String(data.get("password") ?? ""),
};
}

Подключение через index.html и main.ts:

const form = document.querySelector("form");
if (!(form instanceof HTMLFormElement)) throw new Error("no form");

form.addEventListener("submit", (e: SubmitEvent) => {
e.preventDefault();
const values = readForm(form);
const errors = validate(values);
if (Object.keys(errors).length > 0) {
console.log(errors);
return;
}
console.log("OK", values);
});

Разбор:

  • instanceof HTMLFormElement сужает тип — 12.md.
  • События — 20.md; в React — 21.md.

Type-safe клиент REST API

Цель

Загрузить пользователя, описать LoadState, не использовать слепой as User.

type User = { id: string; name: string };

type LoadState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; message: string };

function isUser(v: unknown): v is User {
if (typeof v !== "object" || v === null) return false;
const o = v as Record<string, unknown>;
return typeof o.id === "string" && typeof o.name === "string";
}

async function fetchUser(id: string): Promise<LoadState> {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { status: "error", message: `HTTP ${res.status}` };
}
const raw: unknown = await res.json();
if (!isUser(raw)) {
return { status: "error", message: "Invalid JSON shape" };
}
return { status: "success", data: raw };
} catch (e: unknown) {
return {
status: "error",
message: e instanceof Error ? e.message : "Network error",
};
}
}

UI-рендер (псевдо-DOM или React):

function renderUserState(state: LoadState): string {
switch (state.status) {
case "idle":
return "Готов к загрузке";
case "loading":
return "Загрузка…";
case "success":
return state.data.name;
case "error":
return state.message;
}
}

Разбор:

  • Полный разбор async — 17.md.
  • Для mock API используйте jsonplaceholder или локальный Express — 22.md.

Чеклист готовности проекта

КритерийПроверка
Нет новых anynpm run typecheck
Внешний JSON через unknownесть isUser или Zod
Состояния загрузкиunion, не три флага
Ошибки обработаныResult или LoadState.error
README с командамиnpm run build, dev

Частые ошибки в учебных проектах

ОшибкаЧто делать
as User на json()guard
let todos: any[]Todo[]
Игнорировать find → undefinedif (!todo)
Один файл на 500 строкмодули todo.ts, api.ts
Нет strictвключить в tsconfig6.md

Итоговая практика

  1. Завершите Todo CLI с командой list / add / done.
  2. Добавьте в форму поле повтор пароля и типизированную валидацию.
  3. Подключите реальный или mock API и отобразите все 4 LoadState.
  4. Вынесите типы в src/types.ts и используйте import type.
  5. Добавьте typecheck в package.json и прогоните перед сдачей.

Смежные статьи