10 Advanced TypeScript Tips That Will Improve Your Code
Advanced TypeScript techniques we use daily: utility types, inference, guards and patterns that will make your code safer and more maintainable.
TypeScript is much more than adding : string to your variables. These are the advanced tips we use at Fluxer Labs to write safer and more expressive code.
1. Const assertions for literals
// Without as const
const config = {
endpoint: '/api/users',
method: 'GET'
};
// type: { endpoint: string, method: string }
// With as const
const config = {
endpoint: '/api/users',
method: 'GET'
} as const;
// type: { readonly endpoint: '/api/users', readonly method: 'GET' }
Useful for configurations that shouldn't change.
2. Discriminated unions for states
// ❌ Bad: confusing optional fields
type ApiResponse = {
data?: User;
error?: string;
loading?: boolean;
};
// ✅ Good: clear and mutually exclusive states
type ApiResponse =
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: string };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'loading':
return <Spinner />;
case 'success':
return <UserCard user={response.data} />; // data exists for sure
case 'error':
return <Error message={response.error} />; // error exists for sure
}
}
3. Template literal types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiVersion = 'v1' | 'v2';
// Generates all combinations automatically
type ApiEndpoint = `/${ApiVersion}/${string}`;
type RequestKey = `${HttpMethod}:${ApiEndpoint}`;
// 'GET:/v1/users', 'POST:/v2/products', etc.
const cache: Record<RequestKey, unknown> = {};
4. Satisfies to validate without losing inference
type Colors = Record<string, [number, number, number]>;
// With type annotation: we lose specific keys
const colors: Colors = {
red: [255, 0, 0],
green: [0, 255, 0],
};
colors.red; // OK
colors.blue; // OK (but doesn't exist!) - TypeScript doesn't know which keys exist
// With satisfies: we validate AND keep inference
const colors = {
red: [255, 0, 0],
green: [0, 255, 0],
} satisfies Colors;
colors.red; // OK
colors.blue; // Error! Property 'blue' does not exist
5. Custom type guards
type Fish = { swim: () => void };
type Bird = { fly: () => void };
// Type guard with 'is'
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows it's Fish
} else {
pet.fly(); // TypeScript knows it's Bird
}
}
6. Infer in conditional types
// Extract return type from a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract element type from an array
type ArrayElement<T> = T extends (infer E)[] ? E : never;
// Extract props from a React component
type PropsOf<T> = T extends React.ComponentType<infer P> ? P : never;
// Usage
type UserProps = PropsOf<typeof UserCard>; // { name: string; age: number }
7. Mapped types with modifiers
type User = {
id: number;
name: string;
email: string;
};
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<PartialUser>;
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// Create a type with only some keys
type UserCredentials = Pick<User, 'email'>;
// Create a type without some keys
type PublicUser = Omit<User, 'email'>;
// Custom: make only some properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type UserWithOptionalEmail = PartialBy<User, 'email'>;
8. Exhaustive checks with never
type Status = 'pending' | 'approved' | 'rejected';
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Waiting...';
case 'approved':
return 'Approved!';
case 'rejected':
return 'Rejected';
default:
// If you add a new status and forget to handle it,
// TypeScript will error here
const _exhaustive: never = status;
return _exhaustive;
}
}
9. Branded types for IDs
// Avoid mixing IDs from different entities
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = createUserId('user-123');
const postId = createPostId('post-456');
getUser(userId); // OK
getUser(postId); // Error! You can't pass PostId where UserId is expected
10. Custom utility type: DeepPartial
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
type Config = {
server: {
port: number;
host: string;
ssl: {
enabled: boolean;
cert: string;
};
};
database: {
url: string;
};
};
// All nested properties are optional
type PartialConfig = DeepPartial<Config>;
const config: PartialConfig = {
server: {
port: 3000,
// host and ssl are optional
},
// database is optional
};
Bonus: Recommended tsconfig configuration
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
Conclusion
TypeScript is a powerful tool when you leverage its full potential. These patterns aren't just for showing off in code reviews - they actually prevent bugs and make code more maintainable.
Start incorporating one or two of these tips in your next project and add more as you feel comfortable.
Want your team to master TypeScript? At Fluxer Labs we offer training and consulting. Contact us.