TypeScript has become the standard for JavaScript development, but many developers only scratch the surface of what it can do. Here are some advanced techniques and configurations that will supercharge your development experience.
Utility Types: Your Secret Weapon
TypeScript's built-in utility types can save you tons of boilerplate code and make your types more expressive.
Pick and Omit for Interface Slicing
interface User {
id: string
name: string
email: string
password: string
createdAt: Date
updatedAt: Date
}
// ✅ Create focused types
type PublicUser = Omit<User, 'password'>
type UserCredentials = Pick<User, 'email' | 'password'>
type CreateUserRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
Partial and Required for Flexibility
// Make all properties optional for updates
function updateUser(id: string, updates: Partial<User>) {
// Implementation
}
// Ensure specific fields are always present
type UserWithRequiredEmail = User & Required<Pick<User, 'email'>>
Advanced Type Patterns
Discriminated Unions for Better Error Handling
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string }
function handleResponse<T>(response: ApiResponse<T>) {
if (response.success) {
// TypeScript knows response.data exists
console.log(response.data)
} else {
// TypeScript knows response.error exists
console.error(response.error)
}
}
Template Literal Types for API Routes
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiEndpoint = '/users' | '/posts' | '/comments'
type ApiRoute = `${HttpMethod} ${ApiEndpoint}`
// Now you have type-safe route definitions
const routes: Record<ApiRoute, Function> = {
'GET /users': getUsersHandler,
'POST /users': createUserHandler,
// TypeScript will error if you miss any routes!
}
Conditional Types for Smart APIs
type ApiCall<T extends string> = T extends `GET ${infer Path}`
? { method: 'GET'; path: Path }
: T extends `POST ${infer Path}`
? { method: 'POST'; path: Path; body: unknown }
: never
// Usage
type GetUsers = ApiCall<'GET /users'> // { method: 'GET'; path: '/users' }
type CreateUser = ApiCall<'POST /users'> // { method: 'POST'; path: '/users'; body: unknown }
Configuration That Actually Helps
Strict TypeScript Config
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
}
}
Path Mapping for Cleaner Imports
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/components/*": ["src/components/*"],
"@/utils/*": ["src/utils/*"],
"@/types/*": ["src/types/*"]
}
}
}
React + TypeScript Best Practices
Component Props with Variants
import { cva, type VariantProps } from 'class-variance-authority'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
destructive: 'bg-destructive text-destructive-foreground',
outline: 'border border-input bg-background',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Generic Components with Constraints
interface SelectOption {
value: string
label: string
}
interface SelectProps<T extends SelectOption> {
options: T[]
value?: T['value']
onChange: (value: T['value']) => void
getOptionLabel?: (option: T) => string
}
function Select<T extends SelectOption>({
options,
value,
onChange,
getOptionLabel = (option) => option.label,
}: SelectProps<T>) {
// Implementation with full type safety
}
Type Guards and Narrowing
Custom Type Guards
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
)
}
// Usage
function processUserData(data: unknown) {
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.name)
}
}
Assertion Functions
function assertIsUser(obj: unknown): asserts obj is User {
if (!isUser(obj)) {
throw new Error('Object is not a valid User')
}
}
// Usage
function handleUserData(data: unknown) {
assertIsUser(data)
// TypeScript knows data is User for the rest of the function
return data.email
}
IDE Integration Tips
JSDoc for Better Intellisense
/**
* Calculates the total price including tax
* @param basePrice - The base price before tax
* @param taxRate - Tax rate as decimal (0.1 for 10%)
* @returns The total price including tax
* @example
* ```typescript
* const total = calculateTotal(100, 0.1); // 110
* ```
*/
function calculateTotal(basePrice: number, taxRate: number): number {
return basePrice * (1 + taxRate)
}
Satisfies Operator for Better Inference
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} satisfies Record<string, string | number>
// TypeScript infers the exact type, not just Record<string, string | number>
// config.apiUrl is string, config.timeout is number
Performance Considerations
Lazy Type Loading
// Instead of importing everything
import { ComponentA, ComponentB, ComponentC } from './components'
// Import only what you need
import type { ComponentAProps } from './components/ComponentA'
const ComponentA = lazy(() => import('./components/ComponentA'))
Type-only Imports
// ✅ Type-only import (no runtime cost)
import type { User } from './types'
// ❌ Regular import (includes runtime code)
import { User } from './types'
The Bottom Line
TypeScript is incredibly powerful when you leverage its advanced features. These patterns and configurations will:
- Catch more bugs at compile time
- Improve IDE experience with better autocomplete
- Make refactoring safer and easier
- Create more maintainable codebases
The key is to gradually adopt these patterns. Start with the utility types and strict configuration, then work your way up to the more advanced patterns as you get comfortable.
Which TypeScript features have had the biggest impact on your development workflow? I'd love to hear about your favorite patterns and tips!