🎯 Зачем спрашивают
- Проверить, понимает ли кандидат, как работает сужение типов в TypeScript.
- Отличает ли он
is(type guard) отas(type assertion).
- Может ли объяснить, что type guard помогает компилятору, но не гарантирует runtime-безопасность.
- Понять, использует ли кандидат type guards на практике: при работе с union-типами, API-ответами, пользовательскими объектами.
- Умеет ли писать свои кастомные type guards, а не только пользоваться
typeofиinstanceof.
📝 Ответ
Коротко
Type guard — это конструкция в коде, которая сужает тип переменной на основе проверки в условии.
Основные виды type guards:
Операторы typeof
Работают для примитивных типов (
string, number, boolean, bigint, symbol, undefined, object, function):function printId(id: string | number) { if (typeof id === "string") { console.log(id.toUpperCase()); // здесь id точно string } else { console.log(id.toFixed(2)); // здесь id точно number } }
У
typeof есть особенности, которые стоит помнить. Если вызвать оператор на null , массиве или инстансе класса, к примеру, Date — значение будет object.
Для таких случаев нужны специфичные guards (Array.isArray, instanceof Date).»Оператор instanceof
Используется для проверки принадлежности объекта к классу:
class Dog { bark() {} } class Cat { meow() {} } function speak(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); // TS знает, что это Dog } else { animal.meow(); // TS знает, что это Cat } }
Работает только для объектов, созданных через
new (с прототипами)Проверка на наличие свойства (in)
Удобно для объектов с разными структурами:
type Car = { wheels: number }; type Boat = { sails: number }; function move(vehicle: Car | Boat) { if ("wheels" in vehicle) { console.log("Это машина с", vehicle.wheels, "колёсами"); } else { console.log("Это лодка с", vehicle.sails, "парусами"); } }
in может возвращать true даже для прототипов.
Пример: "toString" in {} → true, хотя это не свойство объекта напрямую.
Более строгой проверкой будет использование
Object.hasOwn.Пользовательские type guards (через is)
Можно создать функцию, которая сама является type guard:
type Fish = { swim: () => void }; type Bird = { fly: () => void }; function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; } function move(pet: Fish | Bird) { if (isFish(pet)) { pet.swim(); // TS знает, что pet — Fish } else { pet.fly(); // TS знает, что pet — Bird } }
⚖️ Компромиссы
- ✅ Позволяют делать код безопаснее и понятнее для TS.
- ❌ Runtime проверка всё равно остаётся на разработчике → type guard не магия, он может быть ложным, если функция написана неверно.
🔎 Встречные вопросы
- Чем
isотличается отas?
- Можно ли написать type guard для generic-типа?
🚩 Красные флаги
- Путать с
as:if ((x as Dog).bark)не сужает тип, а просто делает приведение.
- Думать, что type guard = runtime safety. Нет: он работает только там, где ты реально сделал проверку.
- Забывать про «узкие» проверки:
typeof x === "object"— слишком общий guard, не даёт полезного сужения.
🛠 Практика
Напишите собственные type guards
type User = { id: string; email: string; username: string; } type Admin = User & { type: 'admin'; canEdit: true; } type Visitor = User & { type: 'visitor'; canEdit: false; } function checkAccess(user: User) { if (isAdmin(user)) { console.log(`${user.username} может редактировать`); } else if (isVisitor(user)) { console.log(`${user.username} только просматривает`); } else { console.log("Неизвестный тип пользователя"); } }
Сделайте обобщённый type guard
function isUserOfType<T extends User>(user: User, type: T["type"]): user is T // Чтобы можно было проверять: if (isUserOfType<Admin>(u, "admin")) { ... }
Отрефакторите код
type UserId = string & { readonly brand: unique symbol }; type BaseUser = { id: UserId; name: string; role: "admin" | "editor" | "viewer"; suspended?: boolean; }; type AdminUser = BaseUser & { role: "admin"; canImpersonate?: boolean; auditLevel?: "low" | "high"; }; type EditorUser = BaseUser & { role: "editor"; sections: string[]; canPublish?: boolean; }; type ViewerUser = BaseUser & { role: "viewer"; betaTester?: boolean; }; type User = AdminUser | EditorUser | ViewerUser; export type Article = { id: string; section: string; authorId: UserId; status: "draft" | "review" | "published"; }; export function canEditArticle(user: User, article: Article): boolean { if (user.suspended === true) return false; if (user.role === "admin" && "auditLevel" in user) { return true; } if (user.role === "editor" && Array.isArray((user as EditorUser).sections)) { return (user as EditorUser).sections.includes(article.section); } if (user.role === "viewer" && !("sections" in user)) { return false; } return false; } export function canPublishArticle(user: User, article: Article): boolean { if (user.suspended) return false; if (user.role === "admin" && "canImpersonate" in user) { return true; } if ( user.role === "editor" && "canPublish" in user && (user as EditorUser).canPublish === true ) { return (user as EditorUser).sections.includes(article.section); } if (user.role === "viewer") { return false; } return false; } export function canAccessAdminPanel(user: User): boolean { if (user.suspended === true) return false; if (user.role === "admin" && "auditLevel" in user) { return true; } if (user.role === "editor") { // редакторам нельзя в админку return false; } if (user.role === "viewer") { // вьюерам нельзя в админку return false; } return false; } export function visibleTabs(user: User): string[] { const tabs: string[] = ["Home"]; if (user.suspended) return ["Suspended"]; if (user.role === "admin" && "auditLevel" in user) { tabs.push("Users", "Settings", "Audit"); } if (user.role === "editor" && Array.isArray((user as EditorUser).sections)) { tabs.push("My Articles"); if ((user as EditorUser).canPublish) { tabs.push("Review"); } } if (user.role === "viewer" && !("sections" in user)) { if ((user as ViewerUser).betaTester) { tabs.push("Labs"); } } return tabs; } export function canDeleteUser(actor: User, target: User): boolean { if (actor.suspended) return false; if (actor.role === "admin" && "auditLevel" in actor) { if (target.role === "admin" && "auditLevel" in target) { return false; } return true; } return false; }