Съдържание на курса

Урок 6 — Данни, които идват отвън

От Dorian Chávez · основател на Hábil и архитект на интеграции ·

Време: 2 × 45 мин

Какво изграждаш: валидация на конфигурация и отговори

Какво научаваш: типът не валидира по време на изпълнение: валидирай на границата; типове, изведени от схемата

След урока ще можеш да

  • Разграничаваш надеждните данни вътре в програмата от стойности, които идват от JSON, от променлива на средата или от HTTP отговор.
  • Получаваш външни данни като unknown и ги превръщаш във вътрешен договор само след като са валидирани.
  • Пишеш защити на типа (type guard) и функции за валидация, които дават полезни грешки, без да използват any или твърдения, за да скрият проблеми.
  • Четеш и валидираш списък от услуги от JSON файл, преди да стартираш едновременни заявки.
  • Превръщаш променливите на средата, които винаги пристигат като текст или като липса, във валидни числови конфигурации.
  • Изведеш вътрешния тип от схема за валидация, за да не поддържаш два договора, които си противоречат.

Защо, преди как

До предишния урок revisor вече има списък от Servicio, може да проверява целите си едновременно и превръща очакваните откази в стойности Estado. Всичко това работи много добре, докато всеки обект се изгражда направо във файлове на TypeScript. Ако напишеш { nombre: "catálogo", url: "https://catalogo.example", timeoutMs: 1500 }, компилаторът може да провери, че обектът съдържа трите свойства и че всяко има обещания тип.

Истинската програма не живее само от обекти, написани от този, който програмира. Списъкът от услуги може да идва от JSON файл, който някой от операциите е променил. Портът на API може да идва от променлива на средата, конфигурирана при стартирането на контейнер. Отговорът на отдалечена услуга може да носи JSON, произведен от друго приложение, с друга версия, друга политика за грешки или временен дефект. И в трите случая програмата получава стойности на JavaScript, които не са минали през компилатора на този проект.

Това е важна граница. Вътре в revisor, след валидация, можеш да работиш със Servicio, Estado и Reporte като с известни договори. На границата, преди валидацията, знаеш само че нещо е пристигнало. Може да е обект, масив, null, текст, число или обект, който изглежда правилен, освен за едно свойство. Здравото решение е да направиш тази несигурност изрична: да получиш unknown, да инспектираш стойността по време на изпълнение и да произведеш надеждна данна или грешка, която показва какво трябва да се поправи.

Често се мисли, че анотация на TypeScript решава този проблем. Ако напишеш const servicio = JSON.parse(texto) as Servicio, редакторът престава да показва предупреждения и следващият код може да чете servicio.timeoutMs. Но не е извършена никаква проверка. as Servicio моли компилатора да ти се довери; не изследва JSON, не превръща низ в число и не добавя липсващо свойство. Когато Node изпълни излъчения JavaScript, псевдонимът Servicio вече няма да съществува.

Разликата прилича на тази в Go между десериализирането на JSON и валидирането на struct. Go може да попълни известните полета на структура, но пак трябва да решиш дали получените стойности са приемливи: празен URL, нулев порт или отрицателен лимит могат да се поберат в типовете си и пак да са невалидни конфигурации. TypeScript има допълнителна отговорност: преди дори да твърди, че една стойност има форма на обект, трябва да го провери. Статичният тип пази връзките в твоя код; валидацията пази входа от външния свят.

Границата не е място да повтаряш валидации из цялото приложение. Ако десет функции питат дали timeoutMs е число, получаваш десет версии на едно и също правило и десет различни съобщения. Ако валидираш веднъж, когато зареждаш конфигурацията, останалото получава readonly Servicio[] и се съсредоточава върху проверката, координирането и представянето на резултатите. Валидацията не прави другите отдалечени услуги надеждни; установява къде revisor решава кои данни може да приеме за свои.

Важно е да се различава и структурата от бизнес правилото. Да потвърдиш, че timeoutMs е число, премахва един клас грешки, но пак допуска -50, NaN или 3.14. За този проект лимитът е цяло количество милисекунди и трябва да е положително. Да потвърдиш, че url е низ, също не доказва, че целта отговаря, нито че принадлежи на правилната мрежа; само проверява, че конфигурацията съдържа непразен текст, който следващата стъпка може да тълкува като URL. Всеки слой отговаря на различен въпрос и никой не замества останалите.

Целта на урока не е да изградиш огромна библиотека за валидация. Целта е да научиш ред на работа, който се запазва, когато проектът расте: да определиш изпълнимо правило на границата, да получиш от него надеждна вътрешна стойност и да запазиш ясно обяснение, когато входът не го изпълнява. Този ред ще подготви revisor за API от урок 8 и за таблото от урок 9, където както сървърът, така и браузърът отново ще пресичат граници на данни.

Понятията

Типовете се изтриват; unknown пази правилното съмнение

TypeScript анализира кода, преди да излъчи JavaScript. Псевдонимите, интерфейсите, генеричните параметри и анотациите помагат на компилатора, но не се превръщат в автоматични проверки при изпълнение. Node получава обикновен JavaScript: не може да попита дали един обект „е Servicio“, защото това име не съществува като стойност по време на изпълнение.

JSON.parse превръща JSON текст в стойност на JavaScript. Стандартът позволява тази стойност да бъде всеки от възможните типове в JSON: обект, масив, низ, число, булева стойност или null. Макар да знаеш, че файлът би трябвало да съдържа услуги, това очакване не променя какво е пристигнало. Затова е удобно да пазиш резултата като unknown, преди да го инспектираш.

// fig06_01.ts
interface Servicio {
  nombre: string;
  url: string;
  timeoutMs: number;
}

const bruto: unknown = JSON.parse(
  '{"nombre":"catálogo","url":"https://catalogo.example","timeoutMs":"rápido"}',
);

const supuesto = bruto as Servicio;

console.log(typeof supuesto.timeoutMs);
console.log(supuesto.timeoutMs);
$ npx tsc --strict --target ES2022 --module nodenext fig06_01.ts
$ node fig06_01.js
string
rápido

Фигурата се компилира, защото твърдението нарежда на компилатора да третира стойността като Servicio. Изпълнението обаче показва действителността: timeoutMs остава низ. Ако по-късна функция правеше аритметика с тази стойност, JavaScript би могъл да я преобразува неочаквано или да произведе NaN. Проблемът не започна в аритметичната операция; започна в момента, в който беше заявен договор без доказателство.

unknown не значи, че стойността е безполезна. Значи, че още не можеш да четеш свойства от нея, нито да я извикваш като функция. Това ограничение е полезно, защото те принуждава да направиш проверката на правилното място. След като докажеш, че стойността е ненулев обект, можеш да прегледаш ключовете му; след като докажеш, че един ключ съдържа положително цяло число, можеш да го използваш като лимит.

Не бъркай unknown с any. any изключва проверката и позволява да достъпваш всяко свойство, сякаш е валидно. Удобно е за няколко секунди и скъпо после: външна грешка може тихо да пропътува няколко функции, докато се появи далеч от произхода си. unknown, напротив, държи несигурността видима. Той е подходящият тип за JSON, за стойностите в catch, за съобщения между процеси и за данни, които идват от мрежа.

Вътре в revisor конфигурираният списък е граница. JSON файлът не бива да захранва директно revisarTodos; първо трябва да мине през функция, която доказва, че има списък от използваеми услуги. След тази функция revisarTodos може да запази договора от урок 5: получава колекция от Servicio и връща обещание за състояния. Не е нужно да знае за JSON, нито за неправилно написани свойства.

Защити на типа (type guard): да провериш формата, която JavaScript наистина има

Защитата на типа е функция, която прави проверка по време на изпълнение и чиято сигнатура съобщава на компилатора какво си научил, ако върне true. Най-малката форма, с която да започнеш, е да провериш дали нещо е запис от свойства. typeof valor === "object" не стига, защото в JavaScript typeof null също е "object" и защото масивите са обекти, макар да не представят конфигурацията на услуга.

Защитата за запис сама по себе си не валидира Servicio. Само отваря вратата, за да питаш за свойства по безопасен начин. От нея можеш да вземеш nombre, url и timeoutMs като стойности unknown и да валидираш всяка с нейните правила. Разделянето на тези стъпки не позволява да дадеш прекалено широк смисъл на малка проверка.

Фигура 06_01 използва опростена и променлива версия на Servicio, за да изолира риска на твърдението. От фигура 06_02 нататък моделът си връща трите свойства readonly, защото конфигурацията вече е приета и не бива да се променя по време на проверка.

// fig06_02.ts
type Resultado<T> =
  | { ok: true; valor: T }
  | { ok: false; detalle: string };

interface Servicio {
  readonly nombre: string;
  readonly url: string;
  readonly timeoutMs: number;
}

function esRegistro(valor: unknown): valor is Record<string, unknown> {
  return typeof valor === "object" && valor !== null && !Array.isArray(valor);
}

function leerServicio(valor: unknown): Resultado<Servicio> {
  if (!esRegistro(valor)) {
    return { ok: false, detalle: "debe ser un objeto" };
  }

  const { nombre, url, timeoutMs } = valor;

  if (typeof nombre !== "string" || nombre.trim() === "") {
    return { ok: false, detalle: "nombre debe ser texto no vacío" };
  }

  if (typeof url !== "string" || url.trim() === "") {
    return { ok: false, detalle: "url debe ser texto no vacío" };
  }

  if (
    typeof timeoutMs !== "number" ||
    !Number.isSafeInteger(timeoutMs) ||
    timeoutMs <= 0
  ) {
    return { ok: false, detalle: "timeoutMs debe ser un entero positivo" };
  }

  return { ok: true, valor: { nombre, url, timeoutMs } };
}

for (const entrada of [
  { nombre: "catálogo", url: "https://catalogo.example", timeoutMs: 1500 },
  { nombre: "pagos", url: "https://pagos.example", timeoutMs: "1500" },
]) {
  const resultado = leerServicio(entrada);

  if (resultado.ok) {
    console.log(`${resultado.valor.nombre}: ${resultado.valor.timeoutMs} ms`);
  } else {
    console.log(`inválido: ${resultado.detalle}`);
  }
}
$ npx tsc --strict --target ES2022 --module nodenext fig06_02.ts
$ node fig06_02.js
catálogo: 1500 ms
inválido: timeoutMs debe ser un entero positivo

Функцията връща Resultado<Servicio> и не хвърля изключение при очаквана лоша конфигурация. Това позволява извикващият да реши дали да спре стартирането, да покаже всички грешки или да продължи с конфигурация по подразбиране. За списък от цели, който определя какво ще се проверява, да спреш стартирането с обяснение обикновено е по-добре, отколкото да стартираш само част, без да предупредиш.

Правилото за цяло число използва Number.isSafeInteger, а не само typeof timeoutMs === "number". В JavaScript NaN, Infinity и 2.5 също имат тип number, но нито едно не представя правилно цяло количество милисекунди. Частта timeoutMs <= 0 изразява решение на този проект: нулата не значи „без лимит“; тя е невалидна конфигурация. Ако продуктът трябваше да представи „без лимит“, трябваше да има нарочна алтернатива, а не да се използва двусмислено число.

Вътре в revisor същата тази функция превръща външен обект във вътрешна Servicio. Типът readonly възвръща полезността си след границата: щом конфигурацията е приета, никой не бива да променя името, URL или лимита на вече стартирана проверка. Защитата не валидира дали URL отговаря. Тази проверка принадлежи на асинхронната заявка, която може да произведе EstadoFalla, дори когато конфигурацията е била напълно валидна.

JSON конфигурация: да валидираш целия документ, преди да работиш

Валиден JSON файл може да съдържа неправилни за твоето приложение данни. JSON.parse отговаря само дали текстът следва граматиката на JSON; не знае, че очакваш масив от услуги, нито че имената им трябва да са различни. Например {"timeoutMs":"mil"} е валиден JSON, но не е използваема конфигурация.

Удобно е да разделиш три откази, които обикновено се смесват. Първият е да не можеш да прочетеш файла: може да не съществува или процесът да няма разрешение. Вторият е текстът да не е валиден JSON. Третият е JSON да е синтактично правилен, но да не изпълнява договора на revisor. Всеки изисква различно обяснение, за да се поправи, макар всички да пречат да се стартира проверката.

Следващата програма зарежда файл до модула. Използва node:fs/promises, префиксът, изискван за вградените модули на Node, и URL, относителен спрямо import.meta.url, за да не зависи от директорията, от която е извикан Node. JSON се получава като unknown; масивът се валидира елемент по елемент, преди да бъде върнат.

Тази фигура използва await на най-високото ниво на файла. Запази я в папката figuras/, създадена в урок 1, чийто package.json съдържа {"type":"module"}; така TypeScript и Node я третират като ESM модул. Без тази конфигурация await би бил валиден само вътре във функция async.

[
  {
    "nombre": "catálogo",
    "url": "https://catalogo.example",
    "timeoutMs": 1500
  },
  {
    "nombre": "pagos",
    "url": "https://pagos.example",
    "timeoutMs": 3000
  }
]
// fig06_03.ts
import { readFile } from "node:fs/promises";

type Resultado<T> =
  | { ok: true; valor: T }
  | { ok: false; detalle: string };

interface Servicio {
  readonly nombre: string;
  readonly url: string;
  readonly timeoutMs: number;
}

function esRegistro(valor: unknown): valor is Record<string, unknown> {
  return typeof valor === "object" && valor !== null && !Array.isArray(valor);
}

function leerServicio(valor: unknown): Resultado<Servicio> {
  if (!esRegistro(valor)) {
    return { ok: false, detalle: "debe ser un objeto" };
  }

  const { nombre, url, timeoutMs } = valor;

  if (
    typeof nombre !== "string" ||
    nombre.trim() === "" ||
    typeof url !== "string" ||
    url.trim() === "" ||
    typeof timeoutMs !== "number" ||
    !Number.isSafeInteger(timeoutMs) ||
    timeoutMs <= 0
  ) {
    return { ok: false, detalle: "servicio incompleto o inválido" };
  }

  return { ok: true, valor: { nombre, url, timeoutMs } };
}

function leerServicios(valor: unknown): Resultado<readonly Servicio[]> {
  if (!Array.isArray(valor)) {
    return { ok: false, detalle: "la configuración debe ser un arreglo" };
  }

  const servicios: Servicio[] = [];

  for (const [indice, entrada] of valor.entries()) {
    const resultado = leerServicio(entrada);

    if (!resultado.ok) {
      return {
        ok: false,
        detalle: `servicio ${indice + 1}: ${resultado.detalle}`,
      };
    }

    servicios.push(resultado.valor);
  }

  return { ok: true, valor: servicios };
}

const archivo = new URL("./fig06_03.servicios.json", import.meta.url);
const texto = await readFile(archivo, "utf8");
const resultado = leerServicios(JSON.parse(texto) as unknown);

if (resultado.ok) {
  console.log(`configuración: ${resultado.valor.length} servicios`);
} else {
  console.log(`configuración inválida: ${resultado.detalle}`);
}
$ npx tsc --strict --target ES2022 --module nodenext --types node fig06_03.ts
$ node fig06_03.js
configuración: 2 servicios

От TypeScript 6 нататък @types/node вече не се зарежда автоматично, когато компилираш изолиран файл. Тъй като тази фигура импортира node:fs/promises, --types node включва изрично инсталираните декларации на Node и позволява на TypeScript да разпознае този вграден модул. Флагът носи само типове за компилацията; Node продължава да предоставя модула, когато програмата се изпълнява.

as unknown в края на JSON.parse не твърди, че документът е валиден. Прави обратното: не позволява да се довериш на широкия тип, който библиотеката излага, и принуждава leerServicios да го третира като непроверен вход. Доказателството идва от конкретните проверки на Array.isArray, esRegistro, typeof и Number.isSafeInteger.

Този пример спира валидацията при първата невалидна услуга, за да е функцията кратка. Друга валидна политика е да се натрупат всички проблеми в масив от грешки, особено ако човек ще редактира голям файл и е удобно всичко да се поправи с един прочит. Важното правило не се променя: отчетът не бива да започва, докато не се реши дали цялата конфигурация е приемлива. Да заглушиш невалиден вход и да продължиш може да остави услуги непроверени, без никой да забележи.

Липсва и едно полезно за продукция правило: повтарящи се имена. Урок 4 вече показа как да се открият със Set. Това правило трябва да се изпълни, след като е валидирана формата на всяка услуга, защото едва тогава знаеш, че nombre е низ. Първо превръщаш всеки външен вход в надежден договор; после прилагаш правилата, които свързват няколко услуги помежду им.

Променливи на средата: текст, липса и изрично преобразуване

Променливите на средата също пресичат граница. В Node process.env.PUERTO има тип string | undefined, дори ако човекът, който подготвя внедряването, вярва, че е написал число. Операционната система пренася текст; не съществува числова променлива на средата. Ако стойността е "8080", трябва да я преобразуваш. Ако е "ocho-mil-ochenta", преобразуването трябва да се провали по четим начин.

Не използвай Number(valor) без допълнително решение. Number("") дава 0, Number(" ") също дава 0, а Number("3.5") дава число, макар да не е цял порт. Не използвай и parseInt като пълна валидация: parseInt("3000ms", 10) връща 3000 и мълчаливо приема текст, който вероятно е бил грешка. Проверка на формата преди преобразуването пази договора ясен.

// fig06_04.ts
type Resultado<T> =
  | { ok: true; valor: T }
  | { ok: false; detalle: string };

function leerEnteroPositivo(
  nombre: string,
  valor: string | undefined,
  predeterminado: number,
): Resultado<number> {
  if (valor === undefined) {
    return { ok: true, valor: predeterminado };
  }

  if (!/^[1-9]\d*$/.test(valor)) {
    return {
      ok: false,
      detalle: `${nombre} debe ser un entero positivo`,
    };
  }

  const numero = Number(valor);

  if (!Number.isSafeInteger(numero)) {
    return {
      ok: false,
      detalle: `${nombre} está fuera del rango seguro`,
    };
  }

  return { ok: true, valor: numero };
}

const entorno: Record<string, string | undefined> = {
  PUERTO: "8080",
  REVISOR_MAX_SERVICIOS: "muchos",
};

const puerto = leerEnteroPositivo(
  "PUERTO",
  entorno.PUERTO,
  3000,
);
const maximo = leerEnteroPositivo(
  "REVISOR_MAX_SERVICIOS",
  entorno.REVISOR_MAX_SERVICIOS,
  20,
);

console.log(puerto.ok ? `puerto: ${puerto.valor}` : puerto.detalle);
console.log(maximo.ok ? `máximo: ${maximo.valor}` : `máximo inválido: ${maximo.detalle}`);
$ npx tsc --strict --target ES2022 --module nodenext fig06_04.ts
$ node fig06_04.js
puerto: 8080
máximo inválido: REVISOR_MAX_SERVICIOS debe ser un entero positivo

Обектът entorno прави примера детерминиран. В истинската програма вторият аргумент ще е process.env.PUERTO. Функцията не се нуждае от промяна: продължава да получава низ или undefined и да връща надеждно число или подробност. Стойността по подразбиране се използва само когато променливата липсва; не бива да прикрива присъстваща, но неправилно написана променлива. Липсата може да има безопасна стойност, избрана от проекта; изрична и невалидна конфигурация трябва да поиска поправка.

Вътре в revisor конфигурацията на порта принадлежи на сървъра от урок 8, а конфигурацията на лимита на услугите може да защити стартирането. Не смесвай тези променливи със списъка от Servicio: портът описва как слуша API; списъкът описва кои цели проверява. Малките функции по категории позволяват точни съобщения и не позволяват произволна променлива да свърши като незадължително свойство на услуга.

Валидацията на променливи на средата е и граница на сигурността. Никога не отпечатвай съдържанието на всички променливи, за да дебъгваш неуспешно преобразуване: една среда може да съдържа тайни. За нечувствителна стойност като порт можеш да назовеш променливата, която се е провалила. За бъдеща парола или ключ съобщи, че липсва или е невалидна, без да възпроизвеждаш тайната или част от нея в журнал (log) или в HTTP отговор.

Схеми и изведени типове: едно правило за изпълнение и за компилация

С появата на повече входове да пишеш тип от една страна и независима валидация от друга може да дублира решения. Можеш да промениш Servicio, за да добавиш equipo, и да забравиш да обновиш валидатора; компилаторът би наблюдавал вътрешните конструкции, но външен вход би могъл да пристигне без новото поле. Схемата се стреми да намали това разстояние: тя е стойност, която умее да чете unknown по време на изпълнение и която освен това позволява да се изведе изходният ѝ тип.

Схемата не е магия. Продължава да се нуждае от изрични правила за текст, цели числа, URL и обекти. Разликата е, че функцията за валидация има генеричен договор: получава unknown и предава Resultado<T>. Параметърът T описва стойността, която остава налична след валидацията. Условен тип може да извлече този T от схемата, без да пишеш втора ръчна дефиниция.

Условни типове, infer и картографирани типове, стъпка по стъпка

Условният тип е правило за типове във формата A extends B ? X : Y: ако A е съвместим с B, произвежда X; иначе произвежда Y. infer е запазена дума, която в това сравнение улавя част от типа, която TypeScript може да изведе. Картографираният тип обхожда ключовете на друг тип, за да построи свойство за всеки от тях; формата [K in keyof T] означава „за всеки ключ K на T“. И тримата съществуват само за компилатора: не генерират инструкции на JavaScript.

Този малък пример показва по една част наведнъж. Salida е условен, защото извлича T само когато получи Esquema<T>; infer T назовава този извлечен тип. ValoresDe е картографиран: запазва nombre и timeoutMs от campos, но заменя всяка схема с нейния изход. Компилацията потвърждава, че servicio има и двете свойства с правилния тип, преди Node да отпечата текста.

// fig06_07.ts
type Esquema<T> = { ejemplo: T };

type Salida<E> = E extends Esquema<infer T> ? T : never;

type ValoresDe<T extends Record<string, Esquema<unknown>>> = {
  [K in keyof T]: Salida<T[K]>;
};

const texto: Esquema<string> = { ejemplo: "catálogo" };
const entero: Esquema<number> = { ejemplo: 1500 };

const campos = { nombre: texto, timeoutMs: entero };

type ServicioDerivado = ValoresDe<typeof campos>;

const servicio: ServicioDerivado = {
  nombre: "catálogo",
  timeoutMs: 1500,
};

console.log(`${servicio.nombre}: ${servicio.timeoutMs} ms`);
$ npx tsc --strict --target ES2022 --module nodenext fig06_07.ts
$ node fig06_07.js
catálogo: 1500 ms

Salida<Esquema<string>> се разрешава в string; Salida<Esquema<number>> в number. Затова картографираният тип завършва като { nombre: string; timeoutMs: number }. Ако смениш timeoutMs с текст в крайния обект, компилацията се проваля: това е статичната проверка, която придружава изхода на фигурата. Сега можеш да прочетеш по-компактната форма на Inferir и на { [K in keyof T]: ... }, която използва пълната схема.

Следващата реализация е нарочно малка. Учи връзката между изпълнимо правило и изведения тип; не претендира да замества зряла библиотека за схеми в голяма система. Забележи, че единственото твърдение е вътре в objeto, след като всеки ключ е валидиран. Остава концентрирано в генеричната инфраструктура, а не разпръснато при тези, които консумират външни данни.

// fig06_05.ts
type Resultado<T> =
  | { ok: true; valor: T }
  | { ok: false; detalle: string };

type Esquema<T> = {
  leer(valor: unknown): Resultado<T>;
};

type Inferir<E extends Esquema<unknown>> =
  E extends Esquema<infer T> ? T : never;

function esRegistro(valor: unknown): valor is Record<string, unknown> {
  return typeof valor === "object" && valor !== null && !Array.isArray(valor);
}

const textoNoVacio: Esquema<string> = {
  leer(valor) {
    if (typeof valor === "string" && valor.trim() !== "") {
      return { ok: true, valor };
    }

    return { ok: false, detalle: "debe ser texto no vacío" };
  },
};

const enteroPositivo: Esquema<number> = {
  leer(valor) {
    if (
      typeof valor === "number" &&
      Number.isSafeInteger(valor) &&
      valor > 0
    ) {
      return { ok: true, valor };
    }

    return { ok: false, detalle: "debe ser entero positivo" };
  },
};

function objeto<T extends Record<string, Esquema<unknown>>>(
  campos: T,
): Esquema<{ [K in keyof T]: Inferir<T[K]> }> {
  return {
    leer(valor) {
      if (!esRegistro(valor)) {
        return { ok: false, detalle: "debe ser un objeto" };
      }

      const salida: Record<string, unknown> = {};

      for (const [clave, esquema] of Object.entries(campos)) {
        const resultado = esquema.leer(valor[clave]);

        if (!resultado.ok) {
          return { ok: false, detalle: `${clave}: ${resultado.detalle}` };
        }

        salida[clave] = resultado.valor;
      }

      return {
        ok: true,
        valor: salida as { [K in keyof T]: Inferir<T[K]> },
      };
    },
  };
}

const esquemaServicio = objeto({
  nombre: textoNoVacio,
  url: textoNoVacio,
  timeoutMs: enteroPositivo,
});

type Servicio = Inferir<typeof esquemaServicio>;

const resultado = esquemaServicio.leer({
  nombre: "catálogo",
  url: "https://catalogo.example",
  timeoutMs: 1500,
});

if (resultado.ok) {
  const servicio: Servicio = resultado.valor;
  console.log(`${servicio.nombre}: ${servicio.timeoutMs} ms`);
}
$ npx tsc --strict --target ES2022 --module nodenext fig06_05.ts
$ node fig06_05.js
catálogo: 1500 ms

Inferir<typeof esquemaServicio> не създава нова валидация. Взема изходния тип, който стойността esquemaServicio вече описва. Ако добавиш equipo: textoNoVacio към обекта с полета, изведеният тип ще получи equipo, а валидацията ще го изисква едновременно. Тази връзка намалява честия източник на противоречия между статичната дефиниция и проверката при изпълнение.

Вътре в revisor схема може да описва както входната конфигурация, така и HTTP отговор, който очакваш от конкретна услуга. Валидацията на конфигурацията изгражда Servicio; валидацията на отговора може да изгради договор, специфичен за тази услуга, преди кодът за проверка да извлече полезна информация. Не бива да използваш обща схема, за да се преструваш, че всички отдалечени услуги отговарят еднакво. Всяка граница се нуждае от договора, който наистина обещава, и от оперативните правила, които проектът решава да приеме.

Грешката, която ще видиш

С TypeScript 7.0.2 и strict опитът да четеш свойство директно от unknown дава TS18046. Това е диагностиката, която пази границата: компилаторът знае, че още не си направил проверка при изпълнение.

// fig06_06.ts
function nombreDe(valor: unknown): string {
  return valor.nombre;
}
$ npx tsc --strict --target ES2022 --module nodenext fig06_06.ts
fig06_06.ts(3,10): error TS18046: 'valor' is of type 'unknown'.

TS18046 не се оправя с valor as { nombre: string }. Това твърдение само променя мнението на компилатора и оставя изпълнението също толкова изложено. Първо провери, че стойността е запис, вземи свойството като unknown и провери, че е низ. Защита като esRegistro от фигура 06_02 решава първата част; typeof valor.nombre === "string" решава втората.

Друга честа диагностика се появява, когато се опиташ да използваш променлива на средата като число, без да я преобразуваш. process.env.PUERTO може да липсва и дори когато съществува, остава текст. Ако една функция се нуждае от number, TypeScript не бива да приема string | undefined като заместител. Поправката е да избереш политика: да използваш стойност по подразбиране за липсата, да отхвърлиш стартирането или да преобразуваш и валидираш текста. Никое от тези решения не се изразява с твърдение за тип.

Грешките при JSON имат друга форма, защото се случват по време на изпълнение. JSON.parse хвърля SyntaxError, ако файлът съдържа излишна запетая, липсва кавичка или няма валиден синтаксис на JSON. Прихвани тази грешка близо до четенето на файла и я превърни в съобщение за конфигурация. Не я бъркай с обект с неправилна форма: един файл може да мине JSON.parse и пак да се провали по-късно в leerServicios.

Какво се прави погрешно

  • Да използваш as Servicio върху JSON. Твърдението не инспектира никаква стойност. Може да заглуши компилатора и да забави отказа до отдалечена операция или до част от таблото, която вече няма контекст за първоначалния файл.

  • Да декларираш външния резултат като any. any позволява на свойства, извиквания и преобразувания да напредват без доказателство. На границата това удобство премахва точно проверката, от която програмата се нуждае. Получавай unknown и стеснявай типа с наблюдаеми правила.

  • Да валидираш само с typeof valor === "object". null и масивите налагат да се обработват различни случаи. Един обект също не гарантира изискваните свойства, нито типовете им; той е само първата стъпка на валидация на структура.

  • Да приемаш невалидни числа, защото typeof valor === "number". NaN, безкрайността, дробите и отрицателните стойности са числа за JavaScript. Правилата на домейна трябва да решат кое подмножество представлява валиден лимит, порт или продължителност.

  • Да преобразуваш с parseInt и да приемаш резултата, без да прегледаш целия текст. parseInt("3000ms", 10) приема числов префикс и отхвърля останалото. За конфигурация е за предпочитане да отхвърлиш стойността и да поискаш изрична поправка.

  • Да използваш стойности по подразбиране, за да прикриеш присъстваща, но неправилно написана променлива. Ако PUERTO=abc, тихото стартиране на друг порт създава разлика между намерението и действителния процес. Стойността по подразбиране е за нарочна липса, а не за да замества грешки.

  • Да повтаряш статичната дефиниция и валидатора без ясна връзка. Два списъка от полета могат да се разделят, когато договорът се промени. Схема, която извежда типа, или тестове, които сравняват двете правила, пазят видимо задължението да се обновяват заедно.

  • Да показваш тайни в грешки за конфигурация. Да посочиш името на липсваща променлива може да е полезно; да отпечаташ съдържанието ѝ може да разкрие ключове и пароли в терминали, журнали (log) или HTTP отговори. Проектирай съобщенията така, че да помагат за поправка, без да разкриват чувствителна информация.

Упражнения

Упражнение 1 — Четец на URL

Напиши leerUrl(valor: unknown): Resultado<string>. Тя трябва да приема само непразен текст, който може да се превърне в new URL(valor). Ако текстът не е валиден URL, трябва да върне ok: false с четима подробност. Изпробвай я с https://catalogo.example и с no-es-url.

Упражнение 2 — Списък с уникални имена

Тръгни от leerServicios от фигура 06_03. След като валидираш всеки отделен запис, използвай Set<string>, за да отхвърлиш повтарящи се имена. Грешката трябва да споменава повтореното име. Изпробвай списък с два записа на име pagos и потвърди, че на кода за проверка не се предава частичен списък.

Упражнение 3 — Конфигурация за стартиране

Дефинирай тип ConfiguracionServidor с puerto и maxServicios, и двете положителни цели числа. Напиши leerConfiguracion(entorno: Record<string, string | undefined>): Resultado<ConfiguracionServidor>. Използвай 3000 по подразбиране за порта и 20 за максимума услуги. Присъстваща променлива с невалидно съдържание трябва да произведе грешка, а не да активира стойността по подразбиране.

Упражнение 4 — Схема за наличен отговор

Използвай шаблона Esquema<T> и Inferir, за да създадеш схема на отговор с codigoHttp положително цяло число и duracionMs неотрицателно цяло число. Изведи изходния ѝ тип и напиши функция, която получава този тип и произвежда HTTP 200 en 42 ms. Обясни защо тази функция не бива да получава директно резултата от JSON.parse.

Решения

Решение 1

Функцията първо потвърждава, че е получила текст, и после делегира синтаксиса на URL. Конструкторът може да хвърли, затова се прихваща единствено за да превърне невалиден вход в очаквания резултат на валидацията.

function leerUrl(valor: unknown): Resultado<string> {
  if (typeof valor !== "string" || valor.trim() === "") {
    return { ok: false, detalle: "url debe ser texto no vacío" };
  }

  try {
    new URL(valor);
    return { ok: true, valor };
  } catch {
    return { ok: false, detalle: "url no tiene un formato válido" };
  }
}

Не е нужно тази функция да отправя заявка към мрежата. Правилно оформен URL може да сочи към цел, която не съществува; това е отказ на асинхронната проверка, а не на конфигурацията.

Решение 2

Имената се проверяват, след като всеки отделен обект е произвел Servicio. Така servicio.nombre вече е надежден низ и не е нужно да смесваш валидацията на типове с правилото за уникалност.

function sinDuplicados(
  servicios: readonly Servicio[],
): Resultado<readonly Servicio[]> {
  const nombres = new Set<string>();

  for (const servicio of servicios) {
    if (nombres.has(servicio.nombre)) {
      return {
        ok: false,
        detalle: `nombre duplicado: ${servicio.nombre}`,
      };
    }

    nombres.add(servicio.nombre);
  }

  return { ok: true, valor: servicios };
}

Пълното зареждане може първо да извика leerServicios и, ако успее, да предаде resultado.valor на sinDuplicados. Ако някоя се провали, не се стартира никаква заявка.

Решение 3

Конфигурацията събира решения за стартиране, които не принадлежат на отделна услуга. Всяко преобразуване пази името на променливата в подробността, за да улесни поправката.

type ConfiguracionServidor = {
  puerto: number;
  maxServicios: number;
};

function leerConfiguracion(
  entorno: Record<string, string | undefined>,
): Resultado<ConfiguracionServidor> {
  const puerto = leerEnteroPositivo(
    "PUERTO",
    entorno.PUERTO,
    3000,
  );

  if (!puerto.ok) {
    return puerto;
  }

  const maxServicios = leerEnteroPositivo(
    "REVISOR_MAX_SERVICIOS",
    entorno.REVISOR_MAX_SERVICIOS,
    20,
  );

  if (!maxServicios.ok) {
    return maxServicios;
  }

  return {
    ok: true,
    valor: {
      puerto: puerto.valor,
      maxServicios: maxServicios.valor,
    },
  };
}

Стойността по подразбиране се прилага само в клона valor === undefined. Празен низ или отрицателно число минават по клона за грешка и налагат да се поправи средата.

Решение 4

Изведеният тип съществува само след валидацията на обекта. Функцията за представяне получава вече надежден отговор и не е нужно да повтаря проверки на unknown.

const respuestaDisponible = objeto({
  codigoHttp: enteroPositivo,
  duracionMs: {
    leer(valor: unknown): Resultado<number> {
      if (
        typeof valor === "number" &&
        Number.isSafeInteger(valor) &&
        valor >= 0
      ) {
        return { ok: true, valor };
      }

      return { ok: false, detalle: "debe ser entero no negativo" };
    },
  },
});

type RespuestaDisponible = Inferir<typeof respuestaDisponible>;

function lineaRespuesta(respuesta: RespuestaDisponible): string {
  return `HTTP ${respuesta.codigoHttp} en ${respuesta.duracionMs} ms`;
}

JSON.parse трябва първо да мине през respuestaDisponible.leer. Без тази валидация изведеният тип би бил само статично обещание за стойност, която по време на изпълнение може да има друга форма.

Как разбирам, че съм успял

  • npx tsc --version отпечатва Version 7.0.2.
  • След като компилираш и изпълниш fig06_01.ts, се появяват string и rápido, което показва, че твърдението не трансформира JSON.
  • След като компилираш и изпълниш fig06_02.ts, се появява валидна услуга и после подробността, че timeoutMs трябва да е положително цяло число.
  • След като компилираш и изпълниш fig06_03.ts, се появява точно configuración: 2 servicios.
  • След като компилираш и изпълниш fig06_04.ts, се появяват портът 8080 и грешка за REVISOR_MAX_SERVICIOS.
  • След като компилираш и изпълниш fig06_07.ts, се появява catálogo: 1500 ms; ако смениш timeoutMs с текст, компилацията се проваля, защото типът е изведен от схемите.
  • След като компилираш fig06_06.ts, се появява TS18046 на реда, който се опитва да чете nombre от unknown.
  • В fig06_03.ts, с figuras/package.json конфигуриран като ESM модул, компилацията и изпълнението завършват с configuración: 2 servicios.

За допълнително четене

  • TypeScript Handbook: Narrowing — официална документация за защитите на типа и стесняването на unknown; консултирано на 2 октомври 2026 г.
  • TypeScript Handbook: Conditional Types — официална документация за условните типове и извеждането с infer; консултирано на 2 октомври 2026 г.
  • Node.js: process.env — официална документация за променливите на средата в Node; консултирано на 2 октомври 2026 г.
  • MDN: JSON.parse() — справочник на JavaScript за анализа на JSON текст и синтактичните му грешки; консултирано на 2 октомври 2026 г.
Да обсъдим вашия проект

Предпочитате имейл? Пишете ни на hola@habil.mx