TypeScript 5.9: evolucion continua del sistema de tipos

TypeScript sigue evolucionando a un ritmo impresionante. La versión 5.9 trae mejoras significativas en inferencia de tipos, rendimiento del compilador y nuevas utilidades que hacen el código más expresivo y seguro. En este articulo exploramos las features más relevantes con ejemplos prácticos.

El operador satisfies: uso avanzado

Introducido en TypeScript 4.9, el operador satisfies se ha convertido en una herramienta esencial. Su valor radica en validar que un valor cumple un tipo sin perder la inferencia específica.

El problema sin satisfies

type Routes = Record<string, { path: string; auth: boolean }>;

// Con anotacion de tipo: pierde inferencia específica
const routes: Routes = {
  home: { path: '/', auth: false },
  dashboard: { path: '/dashboard', auth: true },
};

// routes.home.path es "string", no "/"
// No puedes acceder a routes.settings - pero tampoco hay error de typo

La solucion con satisfies

const routes = {
  home: { path: '/', auth: false },
  dashboard: { path: '/dashboard', auth: true },
} satisfies Routes;

// routes.home.path es "/" (literal type preservado)
// routes.settings -> Error de compilación (no existe)

satisfies con funciones de configuración

interface AppConfig {
  apiUrl: string;
  timeout: number;
  features: Record<string, boolean>;
  debug: boolean;
}

const config = {
  apiUrl: 'https://api.bemorex.com',
  timeout: 5000,
  features: {
    darkMode: true,
    analytics: true,
    experimental: false,
  },
  debug: false,
} satisfies AppConfig;

// config.features.darkMode es boolean, no unknown
// Autocompletado para config.features funciona perfectamente

Tipos condicionales avanzados

infer con restricciones

TypeScript permite restringir el tipo inferido con extends:

// Extraer el tipo de retorno solo si es un Promise
type UnwrapPromise<T> =
  T extends Promise<infer U extends object> ? U : never;

type Result = UnwrapPromise<Promise<{ id: number; name: string }>>;
// Result = { id: number; name: string }

type NoResult = UnwrapPromise<Promise<string>>;
// NoResult = never (string no extiende object)

Template literal types en la práctica

// Generar tipos para eventos del DOM
type DomEvent = 'click' | 'focus' | 'blur' | 'input' | 'change';
type EventHandler = `on${Capitalize<DomEvent>}`;
// "onClick" | "onFocus" | "onBlur" | "onInput" | "onChange"

// Rutas tipadas
type ApiRoute = `/api/${string}`;

function fetchData(url: ApiRoute): Promise<unknown> {
  return fetch(url).then(r => r.json());
}

fetchData('/api/users');       // OK
fetchData('/users');           // Error: no empieza con /api/

Tipos mapeados con filtrado

// Extraer solo las propiedades opcionales de un tipo
type OptionalKeys<T> = {
  [K in keyof T]-?: undefined extends T[K] ? K : never
}[keyof T];

interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  bio?: string;
}

type OptionalUserKeys = OptionalKeys<User>;
// "avatar" | "bio"

Const type parameters

Los const type parameters permiten inferir tipos literales en funciones genericas sin que el llamador use as const:

// Sin const: infiere string[]
function createRoute<T extends readonly string[]>(segments: T) {
  return '/' + segments.join('/');
}
const r1 = createRoute(['api', 'users']); // string

// Con const: infiere readonly ["api", "users"]
function createRoute<const T extends readonly string[]>(segments: T) {
  return '/' + segments.join('/') as `/${T[number]}`;
}
const r2 = createRoute(['api', 'users']); // "/api" | "/users"

Uso práctico: builders tipados

function defineConfig<const T extends {
  routes: Record<string, { path: string; component: string }>;
  plugins: readonly string[];
}>(config: T): T {
  return config;
}

const appConfig = defineConfig({
  routes: {
    home: { path: '/', component: 'HomePage' },
    about: { path: '/about', component: 'AboutPage' },
  },
  plugins: ['analytics', 'seo'],
});

// appConfig.routes.home.path es "/" (literal)
// appConfig.plugins es readonly ["analytics", "seo"]

Import attributes

Los import attributes (antes import assertions) permiten especificar metadatos sobre las importaciones:

// Importar JSON con tipo
import config from './config.json' with { type: 'json' };

// Importar CSS module
import styles from './button.module.css' with { type: 'css' };

Mejoras en inferencia de return types

TypeScript 5.9 mejora la inferencia de tipos de retorno en funciones complejas:

// Antes: necesitabas anotar el retorno
function processData(input: string | number) {
  if (typeof input === 'string') {
    return { type: 'text' as const, value: input.trim() };
  }
  return { type: 'number' as const, value: input * 2 };
}

// TypeScript infiere correctamente:
// { type: "text"; value: string } | { type: "number"; value: number }

const result = processData('hola');
if (result.type === 'text') {
  result.value.toUpperCase(); // OK: sabe que es string
}

NoInfer utility type

NoInfer<T> evita que TypeScript infiera un tipo a partir de una posición específica:

// Sin NoInfer: TypeScript infiere T como 'primary' | 'secondary' | 'invalid'
function createTheme<T extends string>(colors: T[], defaultColor: T) {
  return { colors, defaultColor };
}
createTheme(['primary', 'secondary'], 'invalid'); // No da error

// Con NoInfer: TypeScript solo infiere T del primer argumento
function createTheme<T extends string>(colors: T[], defaultColor: NoInfer<T>) {
  return { colors, defaultColor };
}
createTheme(['primary', 'secondary'], 'invalid'); // Error: 'invalid' no es 'primary' | 'secondary'

Patrones avanzados para Angular

Signals tipados fuertemente

import { signal, computed } from '@angular/core';

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
}

// Signal con tipo explícito cuando es necesario
const products = signal<Product[]>([]);

// Computed con inferencia automática
const totalValue = computed(() =>
  products().reduce((sum, p) => sum + p.price * p.stock, 0)
);

// Signal derivado con transformación tipada
const productNames = computed(() =>
  products().map(p => p.name)
); // string[]

Discriminated unions para estado de UI

type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

const state = signal<LoadingState<Product[]>>({ status: 'idle' });

// En el template o computed
const products = computed(() => {
  const s = state();
  return s.status === 'success' ? s.data : [];
});

Configuración recomendada de tsconfig

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "verbatimModuleSyntax": true
  }
}
Opcion Que hace
strict Habilita todas las verificaciones estrictas
noUncheckedIndexedAccess Agrega undefined al acceder por índice
exactOptionalPropertyTypes Distingue undefined de omitido
noImplicitOverride Requiere override keyword en subclases
verbatimModuleSyntax Importaciones más predecibles

Conclusion

TypeScript 5.9 refuerza su posición como el superset de JavaScript más poderoso y pragmatico. Features como satisfies, const type parameters, NoInfer y las mejoras en inferencia hacen que escribir código tipado sea más natural y menos verboso. Adoptar estas features no solo mejora la seguridad del tipo, sino que también mejora la experiencia de desarrollo con mejor autocompletado e información de errores más clara.