TypeScript dominates modern development with near-universal adoption. 2025 brings mature patterns for full-stack type safety. At ZIRA Software, TypeScript eliminated entire categories of runtime errors across our applications.
TypeScript 5.x Features
// Const type parameters (5.0)
function createStore<const T extends readonly string[]>(keys: T) {
return keys;
}
const store = createStore(['user', 'cart', 'settings']);
// Type: readonly ["user", "cart", "settings"]
// Decorators (5.0)
function logged<T extends (...args: any[])=> any>(
target: T,
context: ClassMethodDecoratorContext
) {
return function (...args: Parameters<T>): ReturnType<T> {
console.log(`Calling ${String(context.name)}`);
return target.apply(this, args);
};
}
class UserService {
@logged
getUser(id: string) {
return { id, name: 'John' };
}
}
End-to-End Type Safety
// Shared types between frontend and backend
// packages/shared/types.ts
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
export interface CreateUserInput {
email: string;
name: string;
password: string;
}
export interface ApiResponse<T> {
data: T;
meta?: {
total: number;
page: number;
perPage: number;
};
}
// Backend (Express/Hono)
// api/routes/users.ts
import { Hono } from 'hono';
import type { User, CreateUserInput, ApiResponse } from '@shared/types';
const app = new Hono();
app.get('/users', async (c): Promise<Response> => {
const users = await db.user.findMany();
const response: ApiResponse<User[]> = {
data: users,
meta: { total: users.length, page: 1, perPage: 20 },
};
return c.json(response);
});
app.post('/users', async (c): Promise<Response> => {
const input = await c.req.json<CreateUserInput>();
const user = await db.user.create({ data: input });
return c.json({ data: user });
});
// Frontend
// hooks/useUsers.ts
import type { User, ApiResponse } from '@shared/types';
export function useUsers() {
return useQuery<ApiResponse<User[]>>({
queryKey: ['users'],
queryFn: () => fetch('/api/users').then(r => r.json()),
});
}
Advanced Generic Patterns
// Builder pattern with generics
class QueryBuilder<T extends Record<string, any>, Selected = T> {
private query: Partial<{
select: (keyof T)[];
where: Partial<T>;
orderBy: keyof T;
}> = {};
select<K extends keyof T>(...keys: K[]): QueryBuilder<T, Pick<T, K>> {
this.query.select = keys;
return this as any;
}
where(conditions: Partial<T>): this {
this.query.where = conditions;
return this;
}
orderBy(key: keyof T): this {
this.query.orderBy = key;
return this;
}
execute(): Promise<Selected[]> {
// Execute query
return Promise.resolve([]);
}
}
// Usage - fully typed
const users = await new QueryBuilder<User>()
.select('id', 'name')
.where({ active: true })
.orderBy('createdAt')
.execute();
// Type: Pick<User, 'id' | 'name'>[]
Zod for Runtime Validation
import { z } from 'zod';
// Define schema once, get type automatically
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2).max(100),
role: z.enum(['admin', 'user', 'guest']),
settings: z.object({
notifications: z.boolean(),
theme: z.enum(['light', 'dark', 'system']),
}).optional(),
});
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Runtime validation with full type safety
function createUser(input: unknown): User {
return UserSchema.parse(input);
}
// API route with validation
app.post('/users', async (c) => {
const result = UserSchema.safeParse(await c.req.json());
if (!result.success) {
return c.json({ errors: result.error.flatten() }, 400);
}
const user = await db.user.create({ data: result.data });
return c.json({ data: user });
});
Type-Safe API Clients
// tRPC-style type-safe API
// server/router.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
user: t.router({
list: t.procedure.query(async () => {
return db.user.findMany();
}),
create: t.procedure
.input(UserSchema)
.mutation(async ({ input }) => {
return db.user.create({ data: input });
}),
byId: t.procedure
.input(z.string().uuid())
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input } });
}),
}),
});
export type AppRouter = typeof appRouter;
// client/api.ts
import { createTRPCClient } from '@trpc/client';
import type { AppRouter } from '../server/router';
const client = createTRPCClient<AppRouter>({
url: '/api/trpc',
});
// Fully typed - autocomplete works
const users = await client.user.list.query();
const newUser = await client.user.create.mutate({
email: 'john@example.com',
name: 'John',
role: 'user',
});
Utility Types
// Custom utility types
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
// Extract function parameter/return types
type AsyncReturnType<T extends (...args: any)=> Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never;
// Usage
async function fetchUser(id: string) {
return { id, name: 'John' };
}
type User = AsyncReturnType<typeof fetchUser>;
// { id: string; name: string }
Strict Configuration
// tsconfig.json - Maximum type safety
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"verbatimModuleSyntax": true
}
}
Conclusion
TypeScript in 2025 enables complete type safety from database to frontend. Modern patterns with Zod, tRPC, and strict configuration eliminate runtime type errors while improving developer experience.
Need TypeScript expertise? Contact ZIRA Software for type-safe development.