Съдържание на курса
Урок 4 — Колекции, генерици и грешки
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 2 × 45 мин
Какво изграждаш: списъка с услуги и отчета
Какво научаваш: масиви, Map/Set, генерици, помощни типове; грешки: изключения срещу резултат, unknown в catch
След урока ще можеш да
- Изградиш и обходиш типизиран списък от
Servicio, без да губиш връзката между всяка услуга и нейната конфигурация. - Избираш между масив,
MapиSetспоред въпроса, на койтоrevisorтрябва да отговори. - Напишеш генерична функция, която пази връзката между типа, който получава, и типа, който връща.
- Използваш
Pick,Omit,ReadonlyиRecord, за да извеждаш договори, без да дублираш модела. - Представиш операция, която може да се провали, с дискриминирано обединение за резултат.
- Поправиш небезопасен достъп до стойност
unknownвътре вcatch, без да използвашany.
Защо, преди как
Моделът от предишния урок определи какво е Servicio и какви форми може да има Estado. Това реши важна част от проблема: всяка стойност вече има изрична форма. И все пак един истински revisor не проверява само една услуга. Трябва да пази списък от цели, да проверява всяка от тях, да събира състоянията им и да произвежда отчет, който отговаря на конкретни въпроси: колко услуги са конфигурирани, кои са се провалили, кой резултат принадлежи на каталога и кои имена са се повторили случайно.
В JavaScript е лесно да започнеш с няколко отделни стойности. Можеш да създадеш catalogo, pagos и inventario като независими променливи и после да извикаш функция за всяка от тях. Подходът върши работа за малка демонстрация, но програмата става крехка, щом конфигурацията се промени. Добавянето на услуга те кара да търсиш на няколко места; подреждането на отчета изисква повтаряне на логика; а избягването на повтарящи се имена остава правило, което никой не проверява.
Колекциите позволяват да изразиш, че данните образуват съвкупност с конкретна връзка. Масивът отговаря на „кои са услугите и в какъв ред искам да ги обхождам?“. Map отговаря на „при дадено име, кое е състоянието му?“. Set отговаря на „виждал ли съм вече това име?“. И трите структури могат да съдържат свързани данни, но не вършат една и съща работа. Да избираш структура по навик, вместо по въпроса, на който трябва да отговориш, обикновено дава код, който се чете по-трудно и се къса по-лесно.
Появява се и трудност, която не се вижда при един-единствен тип. revisor ще обработва масиви от услуги, масиви от състояния и може би масиви от съобщения за таблото. Можеш да напишеш отделна функция за всеки масив, но накрая ще копираш една и съща логика. Генеричната функция позволява да опишеш връзката, която се пази, дори когато типът на елементите се променя. Не става дума да заместиш всички типове с една загадъчна буква, а да кажеш точно, че резултатът остава от същия тип като входа.
Накрая, този урок трябва да говори за откази, преди урок 5 да добави асинхронни операции и истински заявки. Една програма може да се провали, защото услуга не е отговорила, защото конфигурацията съдържа несъвместими данни или защото функция е получила нещо неочаквано. JavaScript позволява да хвърлиш почти всякаква стойност с throw: инстанция на Error, низ, число или дори непълен обект. Строгият TypeScript тръгва от тази действителност и третира стойността в catch като unknown. Това решение може отначало да изглежда неудобно, но не позволява самият код за обработка на грешки да се провали, докато се опитва да прочете свойство, което може да не съществува.
В Go функцията обикновено връща стойност и error, а извикващият решава дали може да продължи. JavaScript и TypeScript също имат изключения: една операция може да прекъсне нормалния ход с throw, а друг блок може да прихване изключението с catch. Никой от двата механизма не е автоматично по-добър. Полезната разлика е да решиш какъв вид отказ представлява всеки от тях. Изключението е за изключителен проблем, който минава през няколко слоя, или за съвместимост с библиотека, която вече хвърля грешки. Типизираният резултат е за случаите, в които неуспехът е нормална възможност в домейна и извикващият трябва да вземе видимо решение.
Отчетът на revisor не бива да зависи от това дали невидимо изключение ще прекъсне целия цикъл на проверка. Ако pagos се провали, а catalogo отговори, отчетът все пак трябва да покаже и двата факта. Затова резултатът от проверката на всяка услуга ще се моделира като дискриминирано обединение: успех със стойност или неуспех с подробност. Изключението, ако се появи в по-долен слой, се превръща в този резултат, преди да продължи нататък. Така останалата част от програмата работи с изрични данни, а таблото може да покаже пълен отчет.
Това разделяне избягва и едно фалшиво обещание. TypeScript може да провери, че функция, която връща Resultado<Estado>, връща една от двете декларирани алтернативи. Не може да гарантира, че даден URL съществува, нито че HTTP отговор описва вярно здравето на услугата. Валидацията на външни данни ще дойде в урок 6. Тук ще изградиш вътрешните структури и договори, които правят възможно да получаваш, организираш и отчиташ тези данни, без да бъркаш липсата с успех.
Понятията
Масиви: подреден и типизиран списък
Масивът в TypeScript използва същата структура като масива в JavaScript. Пази реда на добавяне, позволява да се обходят елементите му и има дължина, достъпна чрез length. Разликата е, че TypeScript може да опише какъв вид елементи принадлежат на списъка. Servicio[] означава „масив, чиито елементи са услуги“; не означава „обект, който случайно има няколко подобни свойства“.
Редът е важно свойство. Ако конфигурацията изброява catalogo, pagos и inventario в този ред, отчетът може да го спази, за да намира читателят резултатите там, където очаква. Масивът е подходящ, когато искаш да обходиш всички елементи, да запазиш последователността им или да трансформираш всеки от тях с операции като map, filter и find.
// fig04_01.ts
interface Servicio {
readonly nombre: string;
readonly url: string;
timeoutMs: number;
}
function nombresDe(servicios: readonly Servicio[]): string {
return servicios.map((servicio) => servicio.nombre).join(", ");
}
const servicios: Servicio[] = [
{
nombre: "catálogo",
url: "https://catalogo.example",
timeoutMs: 1500,
},
{
nombre: "pagos",
url: "https://pagos.example",
timeoutMs: 3000,
},
];
servicios.push({
nombre: "inventario",
url: "https://inventario.example",
timeoutMs: 2000,
});
console.log(`cantidad: ${servicios.length}`);
console.log(nombresDe(servicios));
$ npx tsc --strict --target ES2022 --module nodenext fig04_01.ts
$ node fig04_01.js
cantidad: 3
catálogo, pagos, inventario
Анотацията Servicio[] пази както създаването, така и последващите промени. Ако се опиташ да добавиш низ, обект без URL или стойност с timeoutMs от тип низ, компилаторът ще посочи проблема преди изпълнението. Това е особено полезно, защото push променя съществуващия масив: не създаваш нов списък, който може да се провери само в началния му литерал.
Обърни внимание на параметъра на nombresDe: той е readonly Servicio[], а не Servicio[]. Функцията трябва само да чете списъка. Декларирането на параметъра като само за четене показва това намерение и не позволява функцията да направи по погрешка push, pop или да присвои нова стойност на позиция. Това не замразява масива по време на изпълнение; както readonly върху свойство, то е статична защита. Който притежава списъка, решава дали може да го променя; функцията, която само го разглежда, получава изглед с по-малко права.
Вътре в revisor масивът от услуги е източникът на работа. Конфигурацията ще има подреден списък от Servicio, урок 5 ще го обходи, за да стартира едновременни заявки, а отчетът ще пази списък от Estado, за да има таблото представяне, което се показва лесно. Не превръщай този списък в Map само защото всяка услуга има име: ще загубиш декларираната последователност и ще принудиш кода за представяне да избира ред по-късно.
JavaScript допуска несъществуващи позиции и позволява да се чете след края на масива. servicios[10] дава undefined, ако има само три елемента. strictNullChecks прави null и undefined изрични алтернативи, когато договорът вече ги декларира, както при find, но strict сам по себе си не променя типа на достъп по индекс: за TypeScript servicios[10] си остава Servicio. Ако искаш всеки достъп по индекс да се третира като потенциално липсващ, включи noUncheckedIndexedAccess; тогава типът става Servicio | undefined и трябва да го провериш, преди да прочетеш свойство. Фигура 04_08 показва диагностиката, която дава тази опция. Преди да индексираш списък, дошъл отвън, трябва да провериш дължината му или да използваш операция, която съобщава за липсата, като find.
За да изолираме колекциите, фигури 04_01, 04_02 и 04_04 нарочно опростяват крайния модел от урок 3: timeoutMs е променливо, а във фигура 04_02 Estado.servicio е само името като низ. В сглобения revisor Servicio пази конфигурационните си свойства като само за четене, а всяко Estado пази цялата Servicio; тук съкратената форма позволява да се съсредоточиш върху операцията на всяка колекция.
Избягвай и forEach по рефлекс. forEach е полезен, за да изпълниш ефект за всеки елемент, като отпечатване на ред, но не изгражда резултат и не позволява лесно ранен изход. map трансформира всички елементи в друг масив; filter пази само тези, които изпълняват условие; find връща първия или undefined. Да избираш метода според стойността, която произвеждаш, прави по-очевидно какво цели функцията.
Map и Set: справки по ключ и принадлежност без дубликати
Map<K, V> свързва ключ от тип K със стойност от тип V. За разлика от обикновен обект, използван като речник, Map изразява, че целта му е да съхранява динамични съответствия, предлага ясни методи като set, get, has и delete и може да използва ключове, които не са низове. За revisor естественият ключ ще е името на услугата, а стойността ще е нейното състояние.
Map#get връща V | undefined, дори когато типът на стойността не допуска undefined. Причината е вярна: ключът може да не съществува. Не пренебрегвай това обединение. Липсващото състояние означава нещо различно от налично състояние и компилаторът те кара да разрешиш разликата, преди да използваш свойства на резултата.
// fig04_02.ts
type Estado = {
readonly servicio: string;
readonly tipo: "disponible" | "falla";
};
const estados = new Map<string, Estado>();
estados.set("catálogo", { servicio: "catálogo", tipo: "disponible" });
estados.set("pagos", { servicio: "pagos", tipo: "falla" });
const nombres = new Set<string>(["catálogo", "pagos", "catálogo"]);
console.log(`estados: ${estados.size}`);
console.log(`catálogo existe: ${estados.has("catálogo")}`);
console.log(`inventario: ${estados.get("inventario") ?? "sin resultado"}`);
console.log(`nombres únicos: ${[...nombres].join(", ")}`);
$ npx tsc --strict --target ES2022 --module nodenext fig04_02.ts
$ node fig04_02.js
estados: 2
catálogo existe: true
inventario: sin resultado
nombres únicos: catálogo, pagos
Операторът ?? означава „използвай стойността отдясно само ако тази отляво е null или undefined“. Тук той не пита дали състоянието е истина или лъжа; пита дали не съществува. По-добър е от ||, когато валидна стойност може да е 0, false или празен низ. Макар състоянията в примера да са обекти и затова винаги да са истинни стойности, добре е да научиш разликата още сега.
Set<T> пази стойности, без да ги повтаря. Когато добавиш за втори път равен низ, множеството запазва само един запис. Това не подрежда елементите по азбучен ред, нито превръща главните букви в малки: "Pagos" и "pagos" са различни низове. Ако домейнът смята тези имена за равни, трябва нарочно да ги нормализираш, преди да ги добавиш. Структурата не може да отгатне бизнес правилата.
Вътре в revisor Set<string> е полезно, за да провериш, че конфигурацията не повтаря имена. Масивът пази първоначалния списък, а Set служи като опора по време на проверката. Ако добавиш всяко име и размерът на множеството не нарасне, си намерил дубликат. Map<string, Estado> ще е полезен след изготвянето на отчета, ако искаш да получиш състояние по име, без да обхождаш целия списък.
Не използвай Map като универсален заместител на масив. Редът на добавяне при Map е определен, но това не значи, че трябва да управлява представянето на отчета. Не използвай и обект с индексни сигнатури като { [nombre: string]: Estado } само за да избегнеш изучаването на Map. Простият обект е отличен, когато знаеш свойствата му предварително; Map е по-ясен, когато ключовете се появяват динамично по време на операцията.
Генерици: да пазиш информацията за типа, когато преизползваш функция
Генеричната функция използва параметър на типа, по конвенция T, за да изрази връзка между части от сигнатурата си. Той не е стойност, достъпна, докато Node изпълнява програмата; както всички типове в TypeScript, изтрива се при компилацията. Работата му е да позволи на компилатора да следи конкретния тип, който пристига във функцията, и да го запази в резултата.
Без генерик функция, която взима първия елемент на списък, би могла да получава unknown[] и да връща unknown. Това принуждава извикващия да инспектира резултата отново, макар компилаторът вече да е знаел, че списъкът съдържа Servicio. Ако напишеш функцията да получава Servicio[], губиш възможността да я преизползваш с Estado[] или друг списък. Генерикът обединява двете нужди: работи с различни типове и запазва кой тип е избран при всяко извикване.
// fig04_03.ts
function primero<T>(valores: readonly T[]): T | undefined {
return valores[0];
}
const puertos = [443, 8080];
const servicios = ["catálogo", "pagos"];
const primerPuerto = primero(puertos);
const primerServicio = primero(servicios);
console.log(`puerto: ${primerPuerto ?? "ninguno"}`);
console.log(`servicio: ${primerServicio ?? "ninguno"}`);
$ npx tsc --strict --target ES2022 --module nodenext fig04_03.ts
$ node fig04_03.js
puerto: 443
servicio: catálogo
T не означава „каквото и да е, без правила“. Означава „тип, който още не е решен, но е последователен в рамките на това извикване“. При първото извикване TypeScript извежда T като number; затова primerPuerto е number | undefined. При второто извежда string; затова primerServicio е string | undefined. undefined остава, защото празен масив няма първи елемент, независимо от типа на елементите му.
Генериците могат да имат и ограничения. Ако една функция трябва да достъпва nombre, не стига да напишеш <T>, защото не всички стойности имат това свойство. Можеш да напишеш T extends { nombre: string }, за да декларираш минималната нужна възможност. Ограничението не принуждава стойностите да бъдат точно този обект; допуска обекти, които имат поне това свойство. Така се възползваш от структурното типизиране, което видя в урок 3.
Вътре в revisor генерична функция ще е полезна, за да не дублираш инфраструктура. Например отчетът ще може да групира състояния, да намира първия елемент на списък или да обвива успешен резултат, без да губи типа на стойността. Не превръщай функция в генерична само защото можеш. Ако операцията е създадена изключително за Servicio, да използваш Servicio в сигнатурата предава по-добре домейна. Генерикът си струва, когато логиката наистина работи по един и същ начин за различни типове и връзката между типовете има значение за този, който получава резултата.
В Go генерична функция също декларира параметри на типа, макар синтаксисът и някои правила да са различни. Полезната идея и в двата езика е една и съща: не пишеш генерична функция, за да избегнеш да мислиш за договорите ѝ, а за да изразиш, че един договор се повтаря, без да свеждаш всички стойности до прекалено широка форма.
Помощни типове: да извеждаш договори от модела
Помощният тип взема съществуващ тип и произвежда друг тип по време на компилация. Не променя обекти по време на изпълнение. Pick, Omit, Readonly и Record са инструменти, включени в TypeScript, за да изразяват чести връзки, без да копираш ръчно всички свойства на един модел.
Да копираш типове изглежда безобидно, когато Servicio има три свойства. Проблемът идва, когато моделът се промени. Ако добавиш reintentos към Servicio и има три ръчно написани частични копия, тези копия могат да остареят по различни начини. Изведеният договор показва ясно, че зависи от оригинала, и позволява на компилатора да посочи промени, които вече изискват решение.
// fig04_04.ts
interface Servicio {
readonly nombre: string;
readonly url: string;
timeoutMs: number;
}
type ServicioPublico = Omit<Servicio, "url">;
type CambioTimeout = Pick<Servicio, "timeoutMs">;
type ServiciosPorNombre = Record<string, ServicioPublico>;
const cambio: CambioTimeout = { timeoutMs: 2500 };
const visibles: ServiciosPorNombre = {
catálogo: { nombre: "catálogo", timeoutMs: cambio.timeoutMs },
pagos: { nombre: "pagos", timeoutMs: 3000 },
};
console.log(visibles.catálogo.nombre);
console.log(visibles.pagos.timeoutMs);
$ npx tsc --strict --target ES2022 --module nodenext fig04_04.ts
$ node fig04_04.js
catálogo
3000
Pick<Servicio, "timeoutMs"> запазва само избраните свойства. Подходящ е за промяна, която трябва да носи изключително данните, които могат да се променят. Omit<Servicio, "url"> създава изглед без URL, полезен, ако таблото трябва да показва услуги, но не бива да получава това свойство. Само по себе си това не е мярка за сигурност: обектът по време на изпълнение все още може да съдържа URL, ако го изпратиш, без да го трансформираш. Типът не позволява на кода на TypeScript да го използва в рамките на този договор; слоят, който изгражда отговора, трябва да реши кои данни да сериализира.
Readonly<T> прави свойствата от първо ниво на T само за четене. Използвай го, когато функция получава конфигурация, която не бива да се променя. Не е дълбок: ако свойство съдържа друг обект, вътрешните свойства остават променливи, освен ако не ги декларираш също като readonly или не използваш тип, създаден за това. Не извиква и Object.freeze; не променя поведението на JavaScript.
Record<K, V> описва обект, чиито ключове са K и чиито стойности са V. Особено е полезен, за да представиш сериализуема структура, която вече има ключове, известни по тип, или таблица, която ще изпратиш като JSON. За динамична колекция, която ще управляваш с методи като has и delete, Map обикновено предава по-добре намерението. Разликата не е в автоматична производителност, а в операциите и значението.
Вътре в revisor Omit<Servicio, "url"> може да определи безопасния изглед, който ще стигне до таблото, Pick може да представи ограничена промяна на таймаута, а Record<string, Estado> може да служи като форма на отчета, индексирана по име, когато HTTP договорът наистина се нуждае от JSON обект. Централният модел остава Servicio; помощните типове са производни изгледи за конкретни случаи, а не анонимни заместители, които скриват домейна.
Грешки: изключение, за да прекъснеш; резултат, за да продължиш
Изключението променя нормалния ход. Когато функция изпълни throw, JavaScript търси най-близкия catch, който може да го обработи. Ако не намери такъв, програмата завършва с грешка. Този механизъм е полезен за грешки, които не могат да се решат локално, или за API, които вече съобщават откази чрез изключения.
Проблемът се появява, когато отказът е очаквана алтернатива на операцията. revisor трябва да съобщи, че pagos се е провалила; не му е нужно да изоставя целия отчет. Ако revisarServicio хвърля изключение за всяка недостъпна услуга и никой не го трансформира, първият проблем може да ти попречи да узнаеш състоянието на останалите. За очаквани резултати дискриминираното обединение държи решението в нормалния ход на програмата.
// fig04_05.ts
type Resultado<T> =
| { ok: true; valor: T }
| { ok: false; detalle: string };
function dividir(dividendo: number, divisor: number): Resultado<number> {
if (divisor === 0) {
return { ok: false, detalle: "el divisor no puede ser cero" };
}
return { ok: true, valor: dividendo / divisor };
}
function mostrar(resultado: Resultado<number>): string {
if (resultado.ok) {
return `resultado: ${resultado.valor}`;
}
return `falla: ${resultado.detalle}`;
}
console.log(mostrar(dividir(12, 3)));
console.log(mostrar(dividir(12, 0)));
$ npx tsc --strict --target ES2022 --module nodenext fig04_05.ts
$ node fig04_05.js
resultado: 4
falla: el divisor no puede ser cero
Свойството ok е дискриминантът. Когато TypeScript види if (resultado.ok), знае, че в този клон съществува valor; в другия клон знае, че съществува detalle. Не е нужно да пишеш незадължителни свойства като valor?: T и detalle?: string, защото тези незадължителни свойства биха допуснали двусмислени състояния: и двете данни налични или и двете липсващи.
В revisor Resultado<Estado> може да представя вътрешния резултат от една заявка, преди да се включи в отчета. Ако библиотека хвърли мрежово изключение, слой, близък до операцията, може да го прихване, да го превърне в { ok: false, detalle } и да позволи на цикъла да продължи. Урок 5 ще добави обещания и едновременни операции; важната идея вече е налице: отказът на отделна услуга трябва да е информация в отчета, а не непременно край на процеса.
Не превръщай всички грешки в резултати, нито всички алтернативи в изключения. Невалидна конфигурация при стартиране може да е правилна причина да спреш програмата, защото няма надежден цикъл, който да продължи. Липсата на отговор от една от десет услуги, напротив, е точно едно от нещата, които отчетът трябва да покаже. Въпросът не е „кой механизъм изглежда по-модерен?“, а „кой може да се възстанови и каква информация трябва да получи?“.
unknown в catch: да инспектираш, преди да се довериш
JavaScript позволява да хвърлиш всякаква стойност. Макар здравословната конвенция да е да се хвърлят инстанции на Error, чужд код може да направи throw "sin red", throw 503 или throw { mensaje: "falló" }. Затова при strict TypeScript третира променливата на catch като unknown: още нямаш доказателство, че е Error, нито че има свойство message.
Решението не е да смениш типа с any. any изключва проверките точно там, където данните са най-малко надеждни. Решението е да стесниш unknown с проверка, която наистина се изпълнява. error instanceof Error проверява, че стойността принадлежи към йерархията на Error; след това условие TypeScript позволява да четеш message.
// fig04_06.ts
function textoError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
return "falla sin detalle legible";
}
try {
throw new Error("tiempo límite agotado");
} catch (error) {
console.log(textoError(error));
}
try {
throw "servicio no alcanzable";
} catch (error) {
console.log(textoError(error));
}
$ npx tsc --strict --target ES2022 --module nodenext fig04_06.ts
$ node fig04_06.js
tiempo límite agotado
servicio no alcanzable
Функцията textoError концентрира малка, но важна политика: какъв текст ще се показва, когато по-долен слой хвърли нещо. Крайният случай не се опитва да сериализира произволно обект, нито разкрива потенциално чувствителни подробности. В истинско приложение може би ще запазваш повече контекст във вътрешен журнал (log), но съобщението, което стига до отчета или до таблото, трябва да е обмислено.
Вътре в revisor мрежовите заявки от следващия урок ще използват това преобразуване близо до try/catch. Резултатът към останалата част от програмата ще е EstadoFalla с безопасна подробност. Това намалява броя на местата, които трябва да разбират изключения, и не позволява React, API и логиката на отчета да реализират три различни версии на една и съща инспекция.
Грешката, която ще видиш
С TypeScript 7.0.2 и strict достъпът до message без проверка на прихванатата стойност дава TS18046. Грешката не казва, че изключенията са невалидни. Казва, че компилаторът не може да докаже, че прихванатата стойност има свойство message, защото JavaScript позволява да се хвърли всякаква стойност.
// fig04_07.ts
function mensaje(error: unknown): string {
return error.message;
}
$ npx tsc --strict --target ES2022 --module nodenext fig04_07.ts
fig04_07.ts(3,10): error TS18046: 'error' is of type 'unknown'.
Поправката не е твърдение като error as Error. Твърдението само принуждава компилатора да се довери; не проверява нищо, докато програмата работи. Ако някой е хвърлил низ, следващият достъп може да даде undefined или да се провали по друг начин. Използвай проверка като error instanceof Error, преди да прочетеш message, точно както във фигура 04_06.
Нормално е да срещнеш и TS2322, когато се опиташ да запишеш стойност с грешен тип в типизиран масив или Map. Например, ако Map<string, Estado> очаква Estado, чието свойство tipo може да бъде само "disponible" | "falla", низът "correcto" не е съвместим, макар да изглежда, че изразява сходна идея. Диагностиката означава, че речникът на програмата е дефиниран в литерален тип и новият низ не принадлежи към него. Поправи стойността, за да използва договорения литерал, или, ако домейнът наистина е получил ново състояние, промени обединението и обработи новия случай във всички функции, които го използват.
Когато Map#get връща стойност, която може да е undefined, обичайната диагностика не е неудобство от страна на компилатора, а сигнал за дизайн. Липсващият ключ е възможен случай. Реши какво трябва да се случи: да върнеш резултат за неуспех, да използваш изрична стойност по подразбиране, да спреш операцията или да провериш has, преди да четеш. Не използвай !, за да изтриеш undefined, освен ако не можеш да докажеш локално, че ключът съществува, и доказателството е до достъпа.
Ако си включил noUncheckedIndexedAccess в проекта, компилаторът прилага същата предпазливост към достъп по индекс. Тази опция не принадлежи към strict: добави я изрично в tsconfig.json, когато проектът индексира масиви или таблици с индекси, които не може да докаже, че са валидни. Резултатът е допълнителна проверка, която не позволява да третираш като съществуващ елемент, който JavaScript може да върне като undefined.
// fig04_08.ts
interface Servicio {
readonly nombre: string;
readonly url: string;
timeoutMs: number;
}
const servicios: Servicio[] = [];
const nombre = servicios[10].nombre;
console.log(nombre);
$ npx tsc --strict --noUncheckedIndexedAccess --target ES2022 --module nodenext fig04_08.ts
fig04_08.ts(9,16): error TS2532: Object is possibly 'undefined'.
Със същия файл и само strict TypeScript приема достъпа, защото индексът запазва типа Servicio; когато добавиш noUncheckedIndexedAccess, изисква да се погрижиш за undefined. Не използвай тази опция като заместител на валидацията на външни данни: тя само подобрява статичния договор на достъпите до колекции, които вече съществуват в паметта.
Какво се прави погрешно
Да използваш any[], за да „накараш списъка да приема всичко“, премахва договора точно когато колекцията смесва данни от различни места. Списък от услуги не бива да приема числа, низове или частично оформени обекти. Ако има място, където още не познаваш елементите, използвай unknown[] и валидирай всеки на границата; ако програмата вече знае договора, използвай Servicio[].
Да копираш дефиницията на Servicio, за да създадеш изглед за таблото, изглежда по-бързо от Pick или Omit, но създава договори, които се разделят с времето. Проблемът не е само повтореният текст: централният модел може да се промени, а копията да запазят стара версия, без компилаторът да свързва двете неща. Извеждай изглед, когато връзката му с модела е истинска, и използвай нов именуван тип, когато представя различна концепция.
Да използваш Set, за да изградиш отчета, е друга честа грешка. Множеството отговаря дали нещо принадлежи или не принадлежи; не пази състоянието, свързано с всяка услуга. Ако трябва да попиташ „какво състояние е имала pagos?“, трябва ти Map или масив от състояния с търсене. Ако трябва да запазиш реда на показване, запази и подреден списък.
Да хвърляш изключения за очаквания резултат на всяка услуга прави потока на управлението труден за проследяване. Извикващият трябва да налучква кои операции могат да хвърлят, кои грешки да прихване и кои да пропусне. За отказ, който трябва да се появи в отчета, върни алтернатива на резултата или го превърни в EstadoFalla близо до операцията, която се е провалила.
Да прихванеш изключение и да напишеш catch (error) { return error.message; } предполага гаранция, която JavaScript не дава. Особено опасно е, защото кодът за възстановяване може да се провали и да скрие първоначалната причина. Третирай стойността като unknown, провери формата ѝ и запази резервно съобщение за неразпознати стойности.
Накрая, не използвай as или оператора !, за да заглушиш тип, който не ти харесва. map.get(nombre)! твърди, че резултатът съществува, но не създава запис в картата. valor as Estado твърди, че стойността изпълнява модела, но не валидира JSON, нито HTTP отговор. Тези инструменти имат точкови приложения, когато вече има доказателство, което компилаторът не може да изведе; не заместват проверка, нито решение за дизайн.
Упражнения
Упражнение 1 — Открий повтарящите се имена
Напиши функция nombresDuplicados(servicios: readonly Servicio[]): string[]. Тя трябва да обходи списъка, да открие имена, които се появяват повече от веднъж, и да върне всяко повтарящо се име само по веднъж. Използвай един Set за видените имена и друг за дубликатите. Изпробвай функцията с catálogo, pagos, catálogo и inventario; изходът трябва да съдържа само catálogo.
Упражнение 2 — Намери услуга по име
Напиши buscarServicio(servicios: readonly Servicio[], nombre: string): Servicio | undefined. Тя трябва да върне услугата, чието име съвпада точно, или undefined, ако не съществува. След това напиши един ред, който показва намерения URL или текста servicio no configurado. Не използвай твърдение за тип, за да премахнеш случая undefined.
Упражнение 3 — Превърни масив в индекс
Напиши генерична функция porClave<T extends { nombre: string }>(valores: readonly T[]): Map<string, T>. Тя трябва да създаде Map, чийто ключ е nombre, а стойността е оригиналният обект. Изпробвай я както с масив от Servicio, така и с масив от обекти, които имат nombre и друго различно свойство.
Упражнение 4 — Превърни изключение в резултат
Дефинирай Resultado<T> с алтернативите ok: true и ok: false. Напиши ejecutar<T>(operacion: () => T): Resultado<T>, за да изпълняваш синхронна операция. Ако операцията върне стойност, трябва да произведе успех; ако хвърли каквато и да е стойност, трябва да върне неуспех с подробност, получена от функция, която приема unknown. Изпробвай операция, която връща 200, и друга, която хвърля new Error("sin conexión").
Решения
Решение 1
interface Servicio {
readonly nombre: string;
readonly url: string;
timeoutMs: number;
}
function nombresDuplicados(servicios: readonly Servicio[]): string[] {
const vistos = new Set<string>();
const duplicados = new Set<string>();
for (const servicio of servicios) {
if (vistos.has(servicio.nombre)) {
duplicados.add(servicio.nombre);
}
vistos.add(servicio.nombre);
}
return [...duplicados];
}
const servicios: Servicio[] = [
{ nombre: "catálogo", url: "https://catalogo.example", timeoutMs: 1500 },
{ nombre: "pagos", url: "https://pagos.example", timeoutMs: 3000 },
{ nombre: "catálogo", url: "https://catalogo.example", timeoutMs: 1500 },
{ nombre: "inventario", url: "https://inventario.example", timeoutMs: 2000 },
];
console.log(nombresDuplicados(servicios).join(", "));
Функцията разделя два въпроса. vistos отговаря дали името вече се е появявало; duplicados не позволява да се добави няколко пъти към резултата. Ако имаше три записа с име catálogo, резултатът пак щеше да е един низ.
Решение 2
interface Servicio {
readonly nombre: string;
readonly url: string;
timeoutMs: number;
}
function buscarServicio(
servicios: readonly Servicio[],
nombre: string,
): Servicio | undefined {
return servicios.find((servicio) => servicio.nombre === nombre);
}
const servicios: Servicio[] = [
{ nombre: "catálogo", url: "https://catalogo.example", timeoutMs: 1500 },
];
const encontrado = buscarServicio(servicios, "pagos");
console.log(encontrado?.url ?? "servicio no configurado");
find изразява точно договора: може да намери елемент или да не намери нито един. Незадължителното верижно свързване ?. не позволява да се чете url, когато encontrado е undefined; ?? осигурява резервния текст.
Решение 3
function porClave<T extends { nombre: string }>(
valores: readonly T[],
): Map<string, T> {
const indice = new Map<string, T>();
for (const valor of valores) {
indice.set(valor.nombre, valor);
}
return indice;
}
const servicios = porClave([
{ nombre: "catálogo", timeoutMs: 1500 },
{ nombre: "pagos", timeoutMs: 3000 },
]);
const equipos = porClave([
{ nombre: "operación", turno: "mañana" },
{ nombre: "soporte", turno: "tarde" },
]);
console.log(servicios.get("pagos")?.timeoutMs);
console.log(equipos.get("soporte")?.turno);
Ограничението изисква свойството, нужно за образуването на ключа, но запазва всички останали свойства. Затова първата Map запазва timeoutMs, а втората запазва turno.
Решение 4
type Resultado<T> =
| { ok: true; valor: T }
| { ok: false; detalle: string };
function textoError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return typeof error === "string" ? error : "falla sin detalle legible";
}
function ejecutar<T>(operacion: () => T): Resultado<T> {
try {
return { ok: true, valor: operacion() };
} catch (error) {
return { ok: false, detalle: textoError(error) };
}
}
console.log(ejecutar(() => 200));
console.log(ejecutar(() => {
throw new Error("sin conexión");
}));
Генеричната функция запазва типа, който връща операцията. Ако операцията произведе число, успешният резултат съдържа число; ако произведеше Estado, щеше да съдържа Estado. Изключението не излиза от ejecutar: превръща се в изрична алтернатива, която извикващият може да покаже или да комбинира с други резултати.
Как разбирам, че съм успял
-
npx tsc --versionотпечатваVersion 7.0.2. - След като изпълниш
npx tsc --strict --target ES2022 --module nodenext fig04_01.tsиnode fig04_01.js, се появява количество от три услуги и имената в редcatálogo,pagosиinventario. - След като изпълниш
npx tsc --strict --target ES2022 --module nodenext fig04_02.tsиnode fig04_02.js, множеството отпечатва две уникални имена, макар да е направен опитcatálogoда се добави два пъти. - След като изпълниш
npx tsc --strict --target ES2022 --module nodenext fig04_05.tsиnode fig04_05.js, се появяват кактоresultado: 4, така иfalla: el divisor no puede ser cero. - След като изпълниш
npx tsc --strict --target ES2022 --module nodenext fig04_07.ts, се появява TS18046 на реда, който се опитва да четеmessageотunknown. - След като изпълниш
npx tsc --strict --noUncheckedIndexedAccess --target ES2022 --module nodenext fig04_08.ts, се появява TS2532 при четене на свойство на индексирания елемент без проверка заundefined.
За допълнително четене
- TypeScript Handbook: Generics — официална документация за параметрите на типа, извеждането и ограниченията; консултирано на 2 октомври 2026 г.
- TypeScript Handbook: Utility Types — официална документация за
Pick,Omit,Readonly,Recordи други помощни типове; консултирано на 2 октомври 2026 г. - TSConfig: useUnknownInCatchVariables — официална документация за използването на
unknownв променливите наcatch; консултирано на 2 октомври 2026 г. - TSConfig: noUncheckedIndexedAccess — официална документация за допълнителната проверка на индексираните достъпи; консултирано на 2 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx