TypeScript continues to evolve with powerful new features that improve type safety and developer experience. This guide covers the most impactful TypeScript 5.x features and patterns you should be using in 2026.
The satisfies Operator
The satisfies operator validates that an expression matches a type while preserving its most specific type. This is incredibly useful for configuration objects and literal types.
// Without satisfies - loses specific type information
type Colors = 'red' | 'green' | 'blue';
type ColorConfig = Record<Colors, string | number[]>;
const colors1: ColorConfig = {
red: '#ff0000',
green: [0, 255, 0],
blue: '#0000ff',
};
// colors1.red is string | number[] - we lost the specific type
colors1.red.toUpperCase(); // Error: Property 'toUpperCase' doesn't exist on number[]
// With satisfies - preserves literal types
const colors2 = {
red: '#ff0000',
green: [0, 255, 0],
blue: '#0000ff',
} satisfies ColorConfig;
// colors2.red is string, colors2.green is number[]
colors2.red.toUpperCase(); // Works!
colors2.green.map(x => x); // Works!
// Real-world example: Route configuration
type Route = {
path: string;
component: React.ComponentType;
auth?: boolean;
roles?: string[];
};
type RouteConfig = Record<string, Route>;
const routes = {
home: {
path: '/',
component: HomePage,
},
dashboard: {
path: '/dashboard',
component: DashboardPage,
auth: true,
},
admin: {
path: '/admin',
component: AdminPage,
auth: true,
roles: ['admin'],
},
} satisfies RouteConfig;
// TypeScript knows exactly what keys exist
type RouteKeys = keyof typeof routes; // 'home' | 'dashboard' | 'admin'
// Type-safe route lookup
function getRoute<K extends keyof typeof routes>(key: K) {
return routes[key];
}
const adminRoute = getRoute('admin');
// adminRoute.roles is string[] (not string[] | undefined)Const Type Parameters
The const modifier on type parameters infers literal types instead of widened types, eliminating the need for 'as const' in many cases.
// Without const - types are widened
function createRoute<T extends string>(path: T) {
return { path };
}
const route1 = createRoute('/users');
// route1.path is string (widened)
// With const - literal types are preserved
function createRouteConst<const T extends string>(path: T) {
return { path };
}
const route2 = createRouteConst('/users');
// route2.path is '/users' (literal)
// Powerful for configuration builders
function defineConfig<const T extends {
routes: Record<string, { path: string; method: 'GET' | 'POST' | 'PUT' | 'DELETE' }>;
middleware?: string[];
}>(config: T): T {
return config;
}
const apiConfig = defineConfig({
routes: {
getUsers: { path: '/users', method: 'GET' },
createUser: { path: '/users', method: 'POST' },
getUser: { path: '/users/:id', method: 'GET' },
},
middleware: ['auth', 'logging'],
});
// Full type inference!
type Routes = typeof apiConfig.routes;
// {
// getUsers: { path: '/users'; method: 'GET' };
// createUser: { path: '/users'; method: 'POST' };
// getUser: { path: '/users/:id'; method: 'GET' };
// }
// Type-safe tuple creation
function tuple<const T extends readonly unknown[]>(...args: T): T {
return args;
}
const t = tuple(1, 'hello', true);
// t is readonly [1, 'hello', true] instead of (string | number | boolean)[]Decorators (Stage 3)
TypeScript 5.0 introduced support for the ECMAScript Stage 3 decorators proposal. These are different from experimental decorators and are the future standard.
// Class decorator
function logged<T extends new (...args: any[]) => any>(target: T, context: ClassDecoratorContext) {
return class extends target {
constructor(...args: any[]) {
console.log(`Creating instance of ${context.name}`);
super(...args);
}
};
}
// Method decorator
function measure<T extends (...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: Parameters<T>): ReturnType<T> {
const start = performance.now();
const result = target.apply(this, args);
const end = performance.now();
console.log(`${String(context.name)} took ${end - start}ms`);
return result;
} as T;
}
// Field decorator
function validate(validator: (value: any) => boolean) {
return function <T>(
target: undefined,
context: ClassFieldDecoratorContext
) {
return function (this: any, initialValue: T): T {
if (!validator(initialValue)) {
throw new Error(`Invalid value for ${String(context.name)}`);
}
return initialValue;
};
};
}
// Accessor decorator
function cached<T>(
target: { get: () => T },
context: ClassAccessorDecoratorContext
) {
return {
get(this: any): T {
const cacheKey = `__cached_${String(context.name)}`;
if (!(cacheKey in this)) {
this[cacheKey] = target.get.call(this);
}
return this[cacheKey];
},
set(this: any, value: T) {
const cacheKey = `__cached_${String(context.name)}`;
delete this[cacheKey];
},
};
}
// Using decorators
@logged
class UserService {
@validate((v) => typeof v === 'string' && v.length > 0)
private apiUrl = 'https://api.example.com';
@cached
accessor config = this.loadConfig();
@measure
async getUsers() {
const response = await fetch(`${this.apiUrl}/users`);
return response.json();
}
private loadConfig() {
// Expensive config loading
return { timeout: 5000 };
}
}Template Literal Types
Template literal types enable powerful string manipulation at the type level, perfect for creating type-safe APIs and configuration.
// Basic template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
// Building a type-safe event system
type Events = 'click' | 'focus' | 'blur' | 'change';
type EventHandlers = {
[K in Events as `on${Capitalize<K>}`]: (event: Event) => void;
};
// { onClick: ..., onFocus: ..., onBlur: ..., onChange: ... }
// Type-safe CSS-in-JS
type CSSProperty = 'margin' | 'padding' | 'border';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSUnit = 'px' | 'rem' | 'em' | '%';
type SpacingProperty = `${CSSProperty}-${CSSDirection}` | CSSProperty;
// 'margin-top' | 'margin-right' | ... | 'margin' | 'padding' | ...
type SpacingValue = `${number}${CSSUnit}`;
// '10px' | '1rem' | etc.
function setSpacing(property: SpacingProperty, value: SpacingValue) {
// Implementation
}
setSpacing('margin-top', '10px'); // Valid
setSpacing('padding', '1rem'); // Valid
// setSpacing('margin-middle', '10px'); // Error!
// Type-safe API routes
type APIVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type APIRoute = `/api/${APIVersion}/${Resource}` | `/api/${APIVersion}/${Resource}/:id`;
function fetchAPI(route: APIRoute) {
return fetch(route);
}
fetchAPI('/api/v1/users'); // Valid
fetchAPI('/api/v2/posts/:id'); // Valid
// fetchAPI('/api/v3/users'); // Error: v3 not valid
// Extracting parts from template literals
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'
// Type-safe route handler
function createHandler<T extends string>(
route: T,
handler: (params: Record<ExtractRouteParams<T>, string>) => void
) {
// Implementation
}
createHandler('/users/:userId/posts/:postId', (params) => {
console.log(params.userId); // Typed!
console.log(params.postId); // Typed!
});Utility Type Patterns
// DeepPartial - make all nested properties optional
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}
function updateConfig(partial: DeepPartial<Config>) {
// Can update any nested property
}
updateConfig({ database: { credentials: { password: 'new' } } });
// DeepReadonly - make all nested properties readonly
type DeepReadonly<T> = T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
// Branded types for type-safe IDs
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = 'abc' as UserId;
const postId = '123' as PostId;
getUser(userId); // OK
// getUser(postId); // Error! PostId is not assignable to UserId
// Strict Object.keys
function typedKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
const user = { name: 'John', age: 30 };
const keys = typedKeys(user); // ('name' | 'age')[]
// Builder pattern with type accumulation
type Builder<T> = {
set<K extends string, V>(key: K, value: V): Builder<T & { [P in K]: V }>;
build(): T;
};
function createBuilder<T = {}>(): Builder<T> {
const obj: Record<string, unknown> = {};
return {
set(key, value) {
obj[key] = value;
return this as any;
},
build() {
return obj as T;
},
};
}
const result = createBuilder()
.set('name', 'John')
.set('age', 30)
.set('active', true)
.build();
// result is { name: string; age: number; active: boolean }Best Practices
TypeScript Best Practices 2026
Use `satisfies` over type annotations when you want validation AND inference
Use `const` type parameters for literal type preservation
Prefer template literal types for string manipulation
Use branded types for type-safe IDs and values
Enable strict mode - always use `strict: true`
Use `unknown` over `any` for type-safe handling
Leverage discriminated unions for state management
Use `NoInfer<T>` to prevent unwanted inference
Conclusion
TypeScript 5.x brings powerful features that enable more expressive and type-safe code. The satisfies operator, const type parameters, and modern decorators are game-changers for building robust applications. Invest time in learning these patterns - they will significantly improve your code quality.
Need help modernizing your TypeScript codebase? Contact Jishu Labs for expert TypeScript consulting and migration services.
About Emily Zhang
Emily Zhang is the Frontend Lead at Jishu Labs with deep expertise in TypeScript and modern web development. She has helped teams adopt TypeScript best practices across large codebases.