Full-Stack Developer & Designer
TypeScript has become the de facto standard for building large-scale JavaScript applications. While many developers know the basics, there are advanced features and patterns that can significantly improve your code quality and developer experience.
TypeScript provides powerful utility types that can transform existing types:
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Create types for different use cases
type CreateUserInput = Omit<User, "id" | "createdAt">;
type UserProfile = Pick<User, "id" | "name" | "email">;
type UpdateUserInput = Partial<Pick<User, "name" | "email">>;
// Make properties optional or required
type OptionalUser = Partial<User>;
type RequiredUser = Required<User>;
Create types that depend on conditions:
type ApiResponse<T> = T extends string
? { message: T }
: T extends number
? { count: T }
: { data: T };
// Usage
type StringResponse = ApiResponse<string>; // { message: string }
type NumberResponse = ApiResponse<number>; // { count: number }
type DataResponse = ApiResponse<User[]>; // { data: User[] }
Generate types from string patterns:
type EventNames = "click" | "focus" | "blur";
type EventHandlers = {
[K in EventNames as `on${Capitalize<K>}`]: (event: Event) => void;
};
// Results in:
// {
// onClick: (event: Event) => void;
// onFocus: (event: Event) => void;
// onBlur: (event: Event) => void;
// }
Limit generic types to specific constraints:
interface Identifiable {
id: string;
}
function updateEntity<T extends Identifiable>(
entities: T[],
id: string,
updates: Partial<T>,
): T[] {
return entities.map((entity) =>
entity.id === id ? { ...entity, ...updates } : entity,
);
}
Transform properties of existing types:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// Results in:
// {
// getId: () => string;
// getName: () => string;
// getEmail: () => string;
// // ... etc
// }
Create functions that narrow types:
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
"email" in obj
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.name);
}
}
Functions that assert a condition or throw:
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== "number") {
throw new Error("Expected number");
}
}
function calculateTax(amount: unknown) {
assertIsNumber(amount);
// TypeScript knows amount is number here
return amount * 0.1;
}
Provide multiple type signatures for the same function:
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
// TypeScript provides precise return types
const div = createElement("div"); // HTMLDivElement
const span = createElement("span"); // HTMLSpanElement
const input = createElement("input"); // HTMLInputElement
Type functions that return other functions:
type EventHandler<T> = (event: T) => void;
type EventListenerAdder<T> = (handler: EventHandler<T>) => () => void;
function createEventListener<T>(
element: EventTarget,
eventType: string,
): EventListenerAdder<T> {
return (handler: EventHandler<T>) => {
const listener = (event: Event) => handler(event as T);
element.addEventListener(eventType, listener);
// Return cleanup function
return () => element.removeEventListener(eventType, listener);
};
}
Type your environment variables:
interface Environment {
NODE_ENV: "development" | "production" | "test";
API_URL: string;
DATABASE_URL: string;
JWT_SECRET: string;
}
declare global {
namespace NodeJS {
interface ProcessEnv extends Environment {}
}
}
// Usage with validation
function getConfig(): Environment {
const requiredVars: (keyof Environment)[] = [
"NODE_ENV",
"API_URL",
"DATABASE_URL",
"JWT_SECRET",
];
for (const varName of requiredVars) {
if (!process.env[varName]) {
throw new Error(`Missing required environment variable: ${varName}`);
}
}
return process.env as Environment;
}
strict
ModeAlways enable strict mode in your tsconfig.json
:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
// Prefer this
interface User {
id: string;
name: string;
}
// Over this (for object shapes)
type User = {
id: string;
name: string;
};
const themes = ["light", "dark"] as const;
type Theme = (typeof themes)[number]; // 'light' | 'dark'
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
} as const;
// Properties are readonly and have literal types
any
, Use unknown
// Bad
function processData(data: any) {
return data.someProperty;
}
// Good
function processData(data: unknown) {
if (typeof data === "object" && data !== null && "someProperty" in data) {
return (data as { someProperty: unknown }).someProperty;
}
throw new Error("Invalid data structure");
}
TypeScript's type system is incredibly powerful when used effectively. These advanced patterns and practices will help you:
The key is to gradually adopt these patterns as your TypeScript skills grow. Start with the basics and progressively incorporate more advanced techniques as they become relevant to your projects.