Замена `any` у TypeScript: шасць безпечных патэранаў для частых ситуацыяў
Выучыце практычныя, безпечныя за выкарыстоўванне альтэрнатывы для `any` у TypeScript — укладзячы невядомы типы, гэнерыкі, дзялёныя аюніі і ўсеохопныя пераканальвання — для обработкі неперакладзучых дадзеных.
Крыніцы: Stop Using ‘any’ in TypeScript — Here’s the Right Fix Every Time
Невяліка змена правіл, яка захавае вас ад неспакоў у працэ
Кожны разработчык на TypeScript страждаў ад гэтага. У пятнічны вечар, калі застаецца мало часу, дзівныя памылкі компілятора зупіняюць вашу просьбу про падключэння коду, і пасля двадцаті хвілін борбы з вярстаковымі генерікамі та типамі-уніями ўсё, чаго вы хочаце — это запусціць код і закрыць ноутбук.
Тады вы беразеце тры літеры, якія рашаюць усё і нічога:
const payload: any = response.data;
Чырвоныя падкрэсленні зникаюць. Процес складання стае зеленым. Усё выглядае нормальна.
Але гэта не ўсё. Выкарыстоўванне any не рашае прыбліжнасці пад час напісання коду — гэта проста кажа TypeScript, каб той перестаў звяртаць на гэта увагу. Вы выключылі саме тую захоўну сетку, якая спачатку падкрэпіла ваш выбор TypeScript. Кожны раз, калі з’являецца any, ён стварае «дзяро» у вашай паверхні типаў, і гэта «дзяро» тыхо распростраńваецца на кожную функцію і зменную, якія з’яўляюцца ў сувязі з ям.
Хорашая новына ў тым, што вам практычна ніколі не будзе яго справжняя патрэба. Ёсць поясненне таго, чаму any завдае такой шкоды, а пасля — пераканальныя альтэрнатывы для ситуацый, калі разработчыкі часта яго вжываюць.
Што на самай працы значыць any (і чаму гэта мае негатыўны эфект)
Багато разработчыкаў прыпускаюць, што any проста означае «тып гэтага значэння ўсё яшчэ не вядомы».
Тое, што на самай працо падае да кампайляру, — гэта: «Прынікніце перагледваць гэтую зменную і верыце, што все, што я раблю з ёю пасля, будзе правільна».
let user: any = { name: "Alice" };
// TypeScript allows all of these without a single warning:
user.authenticate(); // Runtime crash: user.authenticate is not a function
user.address.postalCode; // Runtime crash: Cannot read properties of undefined
user(); // Runtime crash: user is not a function
Што робіць ситуацыю ўжо горшай, — гэта викорыстанне any. Калі тип значэння, якое вяртае функцыя, ёсьць any, кожная зменная, якая зафіксавае гэта значэння, таксама становіцца any. Калі гэта значэння перадаецца іншай функцыі, прыгожчанне ризыка продовжваецца. Чэрз некалькі часоў вялікія часткі вашага кодавой базы фактычна становяцца звычным JavaScript з расширэнням .ts.
У наступных раздзелах паказаны правільныя альтернатывы для кожнага распашчэтанага случая, калі any пачынае прасачвацца.
Рашэнне 1: Калі тип насправды ўсё яшчэ не вядома (unknown)
Ситуацыя: Вы запоўнюеце даны з зовнішняй API, парсуеце строку JSON, якая была выявлена з локальнага сховішча, або обробляеце необработаны пакет дадзеных з webhook. У такіх случаях немагчыма знайсці точную структуру дадзеных прычыму, пакі ў програме насправды не запускаецца.
Проблематычны падход:
function parseApiResponse(jsonString: string): any {
return JSON.parse(jsonString);
}
const data = parseApiResponse('{"id": 101}');
console.log(data.profile.avatar); // Compiles fine, crashes immediately if profile is missing
Правільны спосаб выправлення: Выкарыстоўваць unknown у поўнанні з type guard.
unknown функцыонуе як болей безпечны аналаг any. Ён паведамляе кампайлеру: „Тып гэтага значэння ўсё яшчэ не вялік, але вам не разрашаецца выкарыстоўваць яго атрыбуты, вызываць методы чы іншым спосабам з яго працаваць, пакі не будзе падтверджана яго рэальная структура.“
interface UserProfile {
id: number;
name: string;
}
// Custom type guard
function isUserProfile(value: unknown): value is UserProfile {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as Record<string, unknown>).id === "number" &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string"
);
}
function parseApiResponse(jsonString: string): unknown {
return JSON.parse(jsonString);
}
const rawData = parseApiResponse('{"id": 101, "name": "Sarah"}');
// rawData.name; // Error: Object is of type 'unknown'.
if (isUserProfile(rawData)) {
// Inside this block, TypeScript knows rawData is definitely UserProfile
console.log(rawData.name); // Fully typed, autocompleted, and safe!
}
Якщо стварэнне ручных механізмаў захавання для складных пакетаў дадзеных здаецца занадто трыткам, вы можетэ викорыстаць бібліятэкі для верыфікацыі пад час выканання, такія як Zod або Valibot. Шырокія бібліятэкі гэтыя верыфікуюць неканальны вхід і ў аднам кроку вырахоўваюць адпаведны тип TypeScript для вас.
Рашэнне 2: Калі адна функцыя павінна падтрымляць калькі типаў (гэнерыкі)
Ситуацыя: Вы ствараеце універсальны інструмент — напрыклад, обгорак для кэшавання, дапаможнік для масоў дадзеных або метод у кліенте API — які павінен працаваць з разнымі форматамі дадзеных.
Проблемны падход:
function getFirstItem(items: any[]): any {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers); // firstNum is typed as 'any'
firstNum.toUpperCase(); // Compiles! Crashes at runtime because it's a number.
Правильнае рашэнне: Выкорыстаць гэнерыкі (синтаксіс <T>).
Гэнерыкі функцыонуюць як месца заместы для типаў, а не для значэнняў. У працоўнасці з гэнерыкамі інфармацыя пра тип не выкарыстоўваецца так, як у any, а гэнерык запамячае тип, заданы вызывачом, і перадае яго да таго момента, калі функцыя вяртае рэзультат.
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers);
// TypeScript automatically infers firstNum as: number | undefined
const names = ["Alice", "Bob"];
const firstName = getFirstItem(names);
// TypeScript automatically infers firstName as: string | undefined
Таксама можна обмежыць тое, каму можа стаць гэнерык, выкарыстоўваючы extends, што гарантуе, што тип будзе мяць пэўныя неабходныя атрыбуты:
interface HasId {
id: string | number;
}
function printId<T extends HasId>(entity: T): void {
console.log(entity.id);
}
Рашэнне 3: Адрабатка аберанняў у блаке try / catch
Ситуацыя: Вы пішаеце логіку ў блаку catch, каб адрабатаць тое, што пайшло не так.
Проблематычны падход:
try {
fetchUserData();
} catch (err: any) {
// Risky: In JavaScript, anything can be thrown (strings, numbers, null)
console.error(err.message.toUpperCase());
}
Рызык тут у там, што JavaScript дазваляе кінуць практычна ўсё — строкі, цыфры, null, звычныя об’екты, а не толькі екземпляры Error. Якщо якая-небудзь бібліятэка або нечаканая памылка кінуе звычную строку, напрыклад throw "Network timeout", тады err.message будзе undefined, і вызванне чаго-небудзь у духу .toUpperCase() на ям можа кінуць новую, неконтрольваную памылку працэсу, які мяў бы караць памылкі.
Правильны спосаб выправлення: Атрыбут падхопленай памылкі задаць як unknown і прызначыць яе тип за дапамою instanceof Error.
try {
fetchUserData();
} catch (err: unknown) {
if (err instanceof Error) {
// TypeScript safely knows 'err' has a .message property
console.error(`Error: ${err.message}`);
} else {
// Fallback for non-standard thrown objects
console.error("An unexpected error occurred", String(err));
}
}
Выправленне 4: Дынамічныя об’екты і пошук па ключоў
Ситуацыя: Патрэбны об’ект, які ведае ся як карточная сістэма або словнік, з ключамі, якія вядомы толькі пад час выканання програмы.
Проблематычны падчынак:
const userRolePermissions: any = {};
userRolePermissions["admin"] = ["read", "write", "delete"];
Правільны спосаб вылечэння: Ожываць Record<Keys, Values>, альбо ж індексаваную падпису.
type Role = "admin" | "editor" | "viewer";
type Permission = "read" | "write" | "delete";
// A strictly defined key-value mapping
const rolePermissions: Record<Role, Permission[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
// If the keys can be arbitrary dynamic strings:
const sessionCache: Record<string, unknown> = {};
Ожыванне Record<string, unknown> дазволяе пераканацца ў форме об’екта, адночасна прымушваючы кожнага, хто яго выкарыстоўвае, пераканацца, што ёсць сама значэнне, прычым яго.
Вылечэнне 5: Указаць, што зменяе форму (Discriminated Unions)
Ситуацыя: Процес праходзіць чераз разныя фазы — напрыклад idle, loading, success і error — і кожная фаза мае свой тип дадзеных.
Проблематычны падчынак:
interface RequestState {
status: any;
data?: any;
error?: any;
}
Моделюванне гэтага з адной лягкай інтарфейсам значыць, што кожная атрыбут стае необавязковым, што змушвае вас на кожным месцы ў кодзе выкарыстоўваць ланцюг ?.. І ўсё гэта не найгорша — ніч не заважае вам опыніцца ў бессэнсным стане, калі як data, так і error заполняюцца адразу.
Правільны спосаб рашэння: выкарыстоўваць дискримінаваныя аюніі.
Да кожнага можлівага стану даўце спяльнае літэральнае поле — часта называемае status або type — якое TypeScript можа выкарыстоўваць, каб розлічыць разныя варіанты:
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };
function renderUI(state: RequestState) {
switch (state.status) {
case "idle":
return "Click start";
case "loading":
return "Loading...";
case "success":
// TypeScript automatically knows state.data exists here!
return `Loaded ${state.data.length} items`;
case "error":
// TypeScript automatically knows state.error exists here!
return `Failed: ${state.error.message}`;
}
}
Рашэння 6: Повная перацэнка з never
Ситуацыя: вы хочаце, каб сам компайлер паведаміў пра гэта, калі колега дадае новы варіант да типу аюніі, але забудзець апдэйтаваць кожны ланцюг switch або if/else, які ім керуецца.
Проблематычны падход: Апелляцыя да загальнага рашэння default: return any, або простае неякшыне распрацоўванне дадатковага кэсу.
Правільны спосаб выправлення: Прызначыце все, што застаўся, типу never.
never описвае значэнне, якое логічна не можа выйсці. Калі ваша інструкцыя switch пакрыла ўсе элементы альянсу, все, што застаўся ў галузі default, становіцца never. Якшо хтось пазней расширыць альянс новым варіантом, кампайлер негаразду пакажае адрасу гэтай лініі, таму што застаўшыся значэнне больш не падходзіць пад never:
type NotificationType = "email" | "sms" | "push";
function sendNotification(type: NotificationType) {
switch (type) {
case "email":
// Handle email
break;
case "sms":
// Handle SMS
break;
case "push":
// Handle push notification
break;
default: {
// If someone adds "in_app" to NotificationType and forgets to update this function,
// TypeScript will raise a compile-time error right here.
const _exhaustiveCheck: never = type;
return _exhaustiveCheck;
}
}
}
Короткая інфармацыя: Замена any
Як застаўіць гэта ў всім калектыва
Розумэнне гэтых заменыяў рашае толькі палову проблемы — другаяя палова — гэта запобежчанне таму, каб any знова не праскраліся, калі падыходзіць канечны термін.
Уключыце гэтыя обмежэння безпасова ў настройкі вашага проекту, каб яны автаматычна дэйствавалі:
У tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
У настройкі ESLint (.eslintrc.json):
{
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
}
Галоўная мета TypeScript — даць вам абяцанне: абяцанне пад час рефакторавання, абяцанне пад час развяртання, а таксама автодапоўненне в маркетплейсе для коду, якае заходзіць за вас і не вымагае постаўлень да дакументацыі. Выкарыстанне any можа здавацца обмежуючым прыблізна першыя калькі дней. Але як толькі на його месца прыходзяць unknown, генерікі і дискримінаваныя юніі, ваш код пачынае сам сабе поясняць, кількасць працоўных суперашчынак рэзкі заходзіць, а кампайлер нарэшце працуе як захоўнік, для каго быў створаны.