TypeScript 5.9: continuous evolution of the type system
TypeScript continues to evolve at an impressive pace. Version 5.9 brings significant improvements in type inference, compiler performance, and new utilities that make code more expressive and safer. In this article we explore the most relevant features with practical examples.
The satisfies operator: advanced usage
Introduced in TypeScript 4.9, the satisfies operator has become an essential tool. Its value lies in validating that a value conforms to a type without losing the specific inference.
The problem without satisfies
type Routes = Record<string, { path: string; auth: boolean }>;
// With type annotation: loses specific inference
const routes: Routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
};
// routes.home.path is "string", not "/"
// You can't access routes.settings - but there's no typo error eitherThe solution with satisfies
const routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
} satisfies Routes;
// routes.home.path is "/" (literal type preserved)
// routes.settings -> Compile error (doesn't exist)satisfies with configuration functions
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 is boolean, not unknown
// Autocompletion for config.features works perfectlyAdvanced conditional types
infer with constraints
TypeScript allows constraining the inferred type with 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 in practice
// 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/Mapped types with filtering
// 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
Const type parameters allow inferring literal types in generic functions without the caller needing 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"Practical use: typed builders
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 is "/" (literal)
// appConfig.plugins is readonly ["analytics", "seo"]Import attributes
Import attributes (formerly import assertions) allow specifying metadata about imports:
// Importar JSON con tipo
import config from './config.json' with { type: 'json' };
// Importar CSS module
import styles from './button.module.css' with { type: 'css' };Improvements in return type inference
TypeScript 5.9 improves return type inference in complex functions:
// 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> prevents TypeScript from inferring a type from a specific position:
// 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'Advanced patterns for Angular
Strongly typed signals
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 for UI state
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 : [];
});Recommended tsconfig configuration
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true
}
}| Option | What it does |
|---|---|
strict |
Enables all strict checks |
noUncheckedIndexedAccess |
Adds undefined when accessing by index |
exactOptionalPropertyTypes |
Distinguishes undefined from omitted |
noImplicitOverride |
Requires override keyword in subclasses |
verbatimModuleSyntax |
More predictable imports |
Conclusion
TypeScript 5.9 reinforces its position as the most powerful and pragmatic JavaScript superset. Features like satisfies, const type parameters, NoInfer, and inference improvements make writing typed code more natural and less verbose. Adopting these features not only improves type safety but also enhances the development experience with better autocompletion and clearer error messages.



Comments (0)
Sign in to comment