If you only use TypeScript to write interfaces and type aliases, you are tapping maybe 20% of its potential. Generics, utility types and conditional types let the compiler enforce complex business rules and catch runtime bugs at compile time. This article unlocks TypeScript's real power.
Generics
Defer the type of a function or type until call time. Code reuse + type safety, together.
// Simple generic
function identity<T>(value: T): T {
return value;
}
identity<number>(42); // T = number
identity('hello'); // T = string (inference)
// Generic constraint
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
getLength('str'); // OK
getLength([1, 2, 3]); // OK
getLength(42); // ERROR
// Multiple type params
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p = pair('age', 30); // [string, number]
Built-in Utility Types
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial — all fields optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; ... }
// Required — all fields required
type StrictUser = Required<UserUpdate>;
// Pick — select specific fields
type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
// Omit — drop specific fields
type UserSafe = Omit<User, 'password'>;
// Readonly
type Frozen = Readonly<User>;
// Record — key-value type
type UsersMap = Record<string, User>;
const users: UsersMap = { 'u1': { ... }, 'u2': { ... } };
ReturnType, Parameters, Awaited
function createUser(data: { name: string; email: string }): Promise<User> { ... }
type CreateUserParams = Parameters<typeof createUser>[0];
// { name: string; email: string }
type CreateUserResult = ReturnType<typeof createUser>;
// Promise<User>
type CreateUserResolved = Awaited<CreateUserResult>;
// User
// Auto-derive the API response type
type ApiUser = Awaited<ReturnType<typeof createUser>>;
Conditional Types
// T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// Distributive conditional types (distribute over unions)
type NonNull<T> = T extends null | undefined ? never : T;
type Result = NonNull<string | null | number>; // string | number
// Real world: filter out nullable fields
type NonNullableKeys<T> = {
[K in keyof T]-?: T[K] extends null | undefined ? never : K
}[keyof T];
The infer Keyword
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[][]>; // number[]
// Extract function return type
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never;
// Promise unwrap
type Unwrap<T> = T extends Promise<infer U> ? U : T;
// Extract from a template literal
type GetFirstWord<S> = S extends `${infer First} ${string}` ? First : S;
type W = GetFirstWord<'Hello World'>; // 'Hello'
Mapped Types
// Transform an entire interface
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
// Make everything nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Deep partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
Template Literal Types
type EventName = `on${Capitalize<'click' | 'change' | 'hover'>}`;
// 'onClick' | 'onChange' | 'onHover'
type Endpoint = `/${'api' | 'admin'}/${'users' | 'posts'}/:id`;
// '/api/users/:id' | '/api/posts/:id' | '/admin/users/:id' | '/admin/posts/:id'
// HTTP method + path narrow type
type Route =
| `GET /${string}`
| `POST /${string}`
| `DELETE /${string}`;
const r: Route = 'GET /users'; // OK
const w: Route = 'PATCH /users'; // ERROR
Discriminated Unions
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: string }
| { status: 'loading' };
function render<T>(res: ApiResponse<T>) {
switch (res.status) {
case 'success': return res.data; // T
case 'error': return res.error; // string
case 'loading': return 'Loading...';
}
}
// TypeScript does an exhaustiveness check — it warns if you skip a case
Type Guards
// Custom type guard
function isUser(x: unknown): x is User {
return typeof x === 'object' && x !== null
&& 'id' in x && typeof x.id === 'number'
&& 'email' in x;
}
// Usage
const data: unknown = fetchData();
if (isUser(data)) {
data.email; // TS now knows it's a User
}
// Zod for runtime + type (industry standard)
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
age: z.number().min(0).max(150)
});
type User = z.infer<typeof UserSchema>; // type is generated automatically
The satisfies Operator
// 4.9+ satisfies: check type compatibility without losing literal types
const config = {
debug: false,
port: 3000,
env: 'production'
} satisfies { debug: boolean; port: number; env: 'development' | 'production' };
config.env; // 'production' (literal) — with 'as' it would widen to string
Practical: Type-Safe Event Emitter
type EventMap = {
'user:created': { id: string; email: string };
'user:deleted': { id: string };
'error': Error;
};
class TypedEmitter<T> {
on<K extends keyof T>(event: K, listener: (data: T[K]) => void) { ... }
emit<K extends keyof T>(event: K, data: T[K]) { ... }
}
const bus = new TypedEmitter<EventMap>();
bus.on('user:created', data => data.email); // OK, data: { id, email }
bus.emit('user:deleted', { id: '42' }); // OK
bus.emit('user:deleted', { email: 'x' }); // ERROR — id is missing
Conclusion
Advanced TypeScript isn't about learning every detail of the language, it's about encoding your own domain as types. Libraries like Zod unite runtime and type. The combo of discriminated unions + type guards + satisfies is the backbone of most modern TS codebases.
Reach out to KEYDAL for type-safe architecture, Zod integration and TS code review. Contact us