Choose Theme

TypeScript Tips and Tricks

· 6 min read · #TypeScript #Advanced #Programming
--

TypeScript has become the de facto standard for building scalable JavaScript applications. Let’s explore some advanced patterns that will level up your TypeScript game.

Type Guards

Type guards help TypeScript narrow down types within conditional blocks:

interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isBird(pet: Bird | Fish): pet is Bird {
return (pet as Bird).fly !== undefined;
}
function move(pet: Bird | Fish) {
if (isBird(pet)) {
pet.fly(); // TypeScript knows pet is Bird here
} else {
pet.swim(); // TypeScript knows pet is Fish here
}
}

User-Defined Type Guards

Create custom type guards for complex scenarios:

type Success<T> = { status: 'success'; data: T };
type Failure = { status: 'error'; error: string };
type Result<T> = Success<T> | Failure;
function isSuccess<T>(result: Result<T>): result is Success<T> {
return result.status === 'success';
}
async function handleResult<T>(result: Result<T>) {
if (isSuccess(result)) {
console.log(result.data); // Type-safe access
} else {
console.error(result.error); // Also type-safe
}
}

Utility Types

TypeScript provides powerful utility types. Here are some essential ones:

Partial and Required

interface User {
id: string;
name: string;
email: string;
age: number;
}
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<PartialUser>;
// Real-world example
function updateUser(id: string, updates: Partial<User>) {
// Can update any subset of user properties
}

Pick and Omit

// Pick specific properties
type UserPreview = Pick<User, 'name' | 'email'>;
// Omit specific properties
type UserWithoutId = Omit<User, 'id'>;
// Combine with other types
type CreateUserDto = Omit<User, 'id'> & {
password: string;
};

Generic Constraints

Generics become powerful with constraints:

interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Usage
interface Product extends HasId {
name: string;
price: number;
}
const products: Product[] = [
{ id: '1', name: 'Widget', price: 9.99 },
{ id: '2', name: 'Gadget', price: 19.99 },
];
const product = findById(products, '1'); // Type: Product | undefined

Conditional Types

Create types that depend on conditions:

type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// Practical example
type Flatten<T> = T extends Array<infer U> ? U : T;
type NumberArray = number[];
type FlatNumber = Flatten<NumberArray>; // number
type FlatString = Flatten<string>; // string

Mapped Types

Transform existing types into new ones:

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface Product {
name: string;
price: number;
}
type ReadonlyProduct = Readonly<Product>;
type NullableProduct = Nullable<Product>;
// {
// name: string | null;
// price: number | null;
// }

Advanced Mapping

// Make all properties mutable
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Make all properties required and non-nullable
type Concrete<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};

Template Literal Types

Build types from string patterns:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/products' | '/orders';
type APIRoute = `${HTTPMethod} ${Endpoint}`;
// Result:
// "GET /users" | "GET /products" | "GET /orders" |
// "POST /users" | "POST /products" | ... etc
// Practical example
type EventName = 'click' | 'focus' | 'blur';
type ElementEventHandler = `on${Capitalize<EventName>}`;
// Result: "onClick" | "onFocus" | "onBlur"

Discriminated Unions

Create type-safe state machines:

type LoadingState = {
status: 'loading';
};
type SuccessState<T> = {
status: 'success';
data: T;
};
type ErrorState = {
status: 'error';
error: Error;
};
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'loading':
console.log('Loading...');
break;
case 'success':
console.log('Data:', state.data); // TypeScript knows data exists
break;
case 'error':
console.log('Error:', state.error.message); // TypeScript knows error exists
break;
}
}

Type Inference

Let TypeScript do the heavy lifting:

// Infer return types
function createUser(name: string, age: number) {
return {
id: Math.random().toString(),
name,
age,
createdAt: new Date(),
};
}
type User = ReturnType<typeof createUser>;
// {
// id: string;
// name: string;
// age: number;
// createdAt: Date;
// }
// Infer parameter types
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]

Best Practices

Here are some tips for writing better TypeScript:

  1. Prefer unknown over any - Forces type checking
  2. Use const assertions - Creates more specific types
  3. Leverage discriminated unions - Better than complex conditionals
  4. Type your errors - Don’t just use Error
  5. Use satisfies operator - Ensures type compliance without widening
// const assertion
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const;
// config.apiUrl is type "https://api.example.com", not string
// satisfies operator (TypeScript 4.9+)
type Theme = {
primary: string;
secondary: string;
};
const theme = {
primary: '#4d9375',
secondary: '#3a7a5e',
// accent: 'blue', // Error: not in Theme type
} satisfies Theme;

Conclusion

TypeScript’s type system is incredibly powerful. These patterns will help you:

  • Write more type-safe code
  • Catch bugs at compile time
  • Improve code documentation
  • Enhance IDE autocomplete

Remember: Types are there to help you, not to fight against. Embrace them!

Keep exploring the TypeScript handbook and don’t be afraid to dive into advanced features. Your future self will thank you!