Patterns TypeScript avancés pour 2024
Les patterns et bonnes pratiques TypeScript pour des projets maintenables et type-safe. Découvrez les techniques utilisées par les meilleurs développeurs.
Introduction
TypeScript a révolutionné le développement JavaScript en apportant la sécurité des types. Mais au-delà des bases, certains patterns avancés peuvent transformer votre code.
Utility Types essentiels
Partial, Required, Readonly
interface User {
id: string
name: string
email: string
avatar?: string
}
// Tous les champs optionnels
type UpdateUser = Partial<User>
// Tous les champs requis
type CompleteUser = Required<User>
// Tous les champs en lecture seule
type ImmutableUser = Readonly<User>
Pick et Omit
// Sélectionner certains champs
type UserPreview = Pick<User, 'id' | 'name'>
// Exclure certains champs
type PublicUser = Omit<User, 'email'>
Patterns avancés
1. Discriminated Unions
Parfait pour gérer différents états :
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function UserProfile({ state }: { state: AsyncState<User> }) {
switch (state.status) {
case 'idle':
return null
case 'loading':
return <Spinner />
case 'success':
return <Profile user={state.data} /> // data est typé!
case 'error':
return <Error message={state.error.message} />
}
}
2. Template Literal Types
Créez des types dynamiques avec des string literals :
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
// Génère: "GET /users" | "GET /posts" | "POST /users" | ...
type APIRoute = `${HTTPMethod} ${Endpoint}`
// Pour les événements
type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// Résultat: "onClick" | "onFocus" | "onBlur"
3. Conditional Types
Types qui changent selon une condition :
type IsArray<T> = T extends any[] ? true : false
type A = IsArray<string[]> // true
type B = IsArray<string> // false
// Plus utile : extraire le type d'un tableau
type ArrayElement<T> = T extends (infer U)[] ? U : never
type Element = ArrayElement<string[]> // string
4. Mapped Types
Transformer tous les champs d'un type :
// Rendre tous les champs nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null
}
// Créer des getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
}
type PersonGetters = Getters<Person>
// { getName: () => string; getAge: () => number }
Patterns pour React
Props avec variants
type ButtonVariant = 'primary' | 'secondary' | 'ghost'
type ButtonSize = 'sm' | 'md' | 'lg'
interface ButtonProps {
variant?: ButtonVariant
size?: ButtonSize
children: React.ReactNode
}
// Avec des props conditionnelles
type LinkButtonProps = ButtonProps & {
as: 'a'
href: string
}
type NormalButtonProps = ButtonProps & {
as?: 'button'
onClick?: () => void
}
type PolymorphicButtonProps = LinkButtonProps | NormalButtonProps
Hooks génériques type-safe
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
})
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value))
}, [key, value])
return [value, setValue] as const
}
// Usage
const [user, setUser] = useLocalStorage<User>('user', defaultUser)
Astuces de pro
1. satisfies operator
Validez un type sans perdre l'inférence :
const config = {
api: 'https://api.example.com',
timeout: 5000,
retries: 3,
} satisfies Record<string, string | number>
// config.api est toujours inféré comme string, pas string | number
2. const assertions
// Sans as const
const routes = ['/', '/about', '/contact']
// Type: string[]
// Avec as const
const routes = ['/', '/about', '/contact'] as const
// Type: readonly ['/', '/about', '/contact']
3. Branded Types
Créez des types nominaux pour plus de sécurité :
type UserId = string & { readonly brand: unique symbol }
type PostId = string & { readonly brand: unique symbol }
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = 'user-123' as UserId
const postId = 'post-456' as PostId
getUser(userId) // ✅ OK
getUser(postId) // ❌ Erreur de type!
Conclusion
Ces patterns TypeScript vous permettront d'écrire du code plus robuste et maintenable. L'investissement initial en apprentissage est largement compensé par la réduction des bugs et l'amélioration de la productivité.