Простые приложения на 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());
Разбор:
find→Todo | undefined— обработкаnot-foundчерезResult— 27.md.- Импорт с
.jsприNodeNext— 9.md.
Расширения
- CLI-аргументы через
process.argvи парсинг команд. - Сохранение в
todos.jsonсreadFile/writeFile— 22.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);
});
Разбор:
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.
Чеклист готовности проекта
| Критерий | Проверка |
|---|---|
Нет новых any | npm run typecheck |
Внешний JSON через unknown | есть isUser или Zod |
| Состояния загрузки | union, не три флага |
| Ошибки обработаны | Result или LoadState.error |
| README с командами | npm run build, dev |
Частые ошибки в учебных проектах
| Ошибка | Что делать |
|---|---|
as User на json() | guard |
let todos: any[] | Todo[] |
Игнорировать find → undefined | if (!todo) |
| Один файл на 500 строк | модули todo.ts, api.ts |
Нет strict | включить в tsconfig — 6.md |
Итоговая практика
- Завершите Todo CLI с командой
list/add/done. - Добавьте в форму поле повтор пароля и типизированную валидацию.
- Подключите реальный или mock API и отобразите все 4
LoadState. - Вынесите типы в
src/types.tsи используйтеimport type. - Добавьте
typecheckвpackage.jsonи прогоните перед сдачей.