07 - Types avec React
Ce que tu vas apprendre
- FC vs typage explicite des props
- Typer useState, useRef, useContext
- Creer des composants génériques
- Typer les event handlers
- Le pattern du composant polymorphique (prop
as) - Typer children, forwardRef et les HOCs
Prerequisites
Avoir lu l'article sur l'error handling type et connaître les bases de React.
FC ou pas FC
React exporte un type FC (FunctionComponent) :
typescriptimport { FC } from "react"
const UserCard: FC<UserCardProps> = ({ name, email }) => {
return <div>{name} - {email}</div>
}
Pendant longtemps, FC incluait children implicitement. Depuis React 18, ce n'est plus le cas. Mais FC a toujours un défaut : il masque le type de retour.
Le pattern que je préféré — et que la plupart des équipes adoptent :
typescriptinterface UserCardProps {
name: string
email: string
onEdit?: (id: string) => void
}
function UserCard({ name, email, onEdit }: UserCardProps) {
return <div>{name} - {email}</div>
}
Pas de FC. Le type des props est explicite. Le type de retour est infere (JSX.Element). C'est plus simple et plus lisible.
Typer children
Trois approches selon ce que tu acceptes :
typescript// 1. ReactNode — accepte tout (string, number, elements, null, undefined)
interface LayoutProps {
children: React.ReactNode
}
// 2. PropsWithChildren — raccourci pour ajouter children optionnel
type LayoutProps = React.PropsWithChildren<{
sidebar: React.ReactNode
}>
// 3. Render prop — children est une fonction
interface DataListProps<T> {
items: T[]
children: (item: T) => React.ReactNode
}
ReactNode est le type le plus large. Il accepte les strings, les numbers, les éléments JSX, les fragments, null, undefined. Pour la plupart des composants de layout, c'est le bon choix.
Typer useState
useState infere le type depuis la valeur initiale :
typescriptconst [count, setCount] = useState(0) // type: number
const [name, setName] = useState("") // type: string
const [user, setUser] = useState<User | null>(null) // doit specifier le generic
Quand la valeur initiale est null ou undefined, TypeScript ne peut pas inferer le type final. Il faut passer le generic explicitement.
typescript// ❌ Infere useState<undefined> — setUser("john") est une erreur
const [user, setUser] = useState(undefined)
// ✅ Specifie le type complet
const [user, setUser] = useState<User | null>(null)
Un pattern courant pour les donnees chargees depuis une API :
typescriptinterface UserPageState {
user: User | null
loading: boolean
error: string | null
}
const [state, setState] = useState<UserPageState>({
user: null,
loading: true,
error: null,
})
Typer useRef
useRef a deux modes selon que la ref est mutable ou non :
typescript// Ref DOM — le type est l'element HTML, initialise a null
const inputRef = useRef<HTMLInputElement>(null)
// type: RefObject<HTMLInputElement> — .current est readonly
// Ref mutable — stocke une valeur quelconque
const intervalRef = useRef<number | null>(null)
// type: MutableRefObject<number | null> — .current est mutable
La distinction depend du fait que tu passes null comme argument avec ou sans le type null dans le generic :
typescript// RefObject (readonly) — null dans l'arg mais pas dans le generic
useRef<HTMLDivElement>(null)
// MutableRefObject — null dans le generic aussi
useRef<HTMLDivElement | null>(null)
Pour les refs DOM que tu attaches a un élément JSX, utilise la première forme (readonly). Pour les refs qui stockent des valeurs (timers, instances), utilise la deuxieme.
Typer useContext
Le context a besoin d'un type et d'une valeur par défaut :
typescriptinterface AuthContext {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const AuthContext = React.createContext<AuthContext | null>(null)
// Hook custom qui garantit que le context est disponible
function useAuth(): AuthContext {
const context = React.useContext(AuthContext)
if (!context) {
throw new Error("useAuth must be used within AuthProvider")
}
return context
}
Le createContext<AuthContext | null>(null) initialise a null. Le hook useAuth fait un narrowing : si le context est null, c'est un bug (le composant n'est pas dans le provider). Apres le check, le type est AuthContext sans null.
Sur paltemps.fr, chaque context a son hook custom. Ca évité de répéter le null check partout.
Composants génériques
Un composant générique accepte un type paramètre :
typescriptinterface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
keyExtractor: (item: T) => string
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
)
}
// Utilisation — T est infere comme User
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
TypeScript infere T depuis le type de items. Les callbacks renderItem et keyExtractor recoivent le bon type sans annotation.
Un pattern plus avance avec contrainte :
typescriptinterface SelectProps<T extends { id: string; label: string }> {
options: T[]
value: T | null
onChange: (option: T) => void
}
function Select<T extends { id: string; label: string }>({
options,
value,
onChange,
}: SelectProps<T>) {
return (
<select
value={value?.id ?? ""}
onChange={(e) => {
const option = options.find(o => o.id === e.target.value)
if (option) onChange(option)
}}
>
{options.map(option => (
<option key={option.id} value={option.id}>
{option.label}
</option>
))}
</select>
)
}
T extends { id: string; label: string } garantit que chaque option a un id et un label. Le composant recoit le type complet en retour — pas seulement { id: string; label: string }.
Typer les event handlers
React fournit des types pour tous les événements :
typescriptfunction Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value) // type: string
}
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY)
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
)
}
Le generic (ex: HTMLInputElement) donne le type de e.currentTarget et e.target. Les types courants :
| Event | Type React |
|---|---|
| onClick | MouseEvent<HTMLElement> |
| onChange | ChangeEvent<HTMLInputElement> |
| onSubmit | FormEvent<HTMLFormElement> |
| onKeyDown | KeyboardEvent<HTMLElement> |
| onFocus | FocusEvent<HTMLElement> |
| onDrag | DragEvent<HTMLElement> |
Le composant polymorphique (prop as)
Un composant qui change son élément HTML selon une prop :
typescripttype PolymorphicProps<E extends React.ElementType> = {
as?: E
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">
function Box<E extends React.ElementType = "div">({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || "div"
return <Component {...props}>{children}</Component>
}
// Utilisation
<Box>default div</Box>
<Box as="section" id="main">section element</Box>
<Box as="a" href="/about">link element</Box>
// href est type-checke parce que as="a" donne les props de <a>
React.ComponentPropsWithoutRef<E> donne les props natives de l'élément E. Si as="a", les props incluent href, target, etc. Si as="button", elles incluent type, disabled, etc.
C'est un pattern avance mais tres utile pour les composants de design system.
Typer forwardRef
forwardRef a besoin du type de la ref et des props :
typescriptinterface InputProps {
label: string
error?: string
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span>{error}</span>}
</div>
)
}
)
Input.displayName = "Input"
// Utilisation
const inputRef = useRef<HTMLInputElement>(null)
<Input ref={inputRef} label="Email" />
Le premier generic est le type de la ref (HTMLInputElement). Le deuxieme est le type des props. Les props natives de <input> sont propagees via ...props.
Résumé
- Prefere le typage explicite des props a
FC— c'est plus simple et plus clair useStateavecnullinitial a besoin du generic :useState<User | null>(null)useRefa deux modes :RefObject(readonly, refs DOM) etMutableRefObject(valeurs)- Les composants génériques inferent le type depuis les props — pas besoin d'annotation
- Les composants polymorphiques utilisent
React.ComponentPropsWithoutRef<E>pour les props natives forwardRefprend deux generics : le type de la ref et le type des props
Article précédent : 06 - Error handling type
Article suivant : 08 - Types avec les ORMs