Six weeks of syntax on functions nobody was going to page you about at 2am. This week is different.
This is props interfaces, event handlers, children, the stuff sitting in every component you
will actually touch on the BBJ rebuild. Everything before this was practice for this.
A props object is just an interface
You already know interface from week two. A component's props are an object, same as any other
object, which means they get the same treatment: name the shape, list each prop with its type,
then destructure that shape straight in the function signature.
The ES6 you write
function NomineeBadge(props) {
return (
<span onClick={props.onSelect}>
{props.name}
</span>
)
}The TS version
interface NomineeBadgeProps {
name: string
onSelect: (e: MouseEvent<HTMLSpanElement>) => void
}
function NomineeBadge({ name, onSelect }: NomineeBadgeProps) {
return (
<span onClick={onSelect}>
{name}
</span>
)
}What changed
props goes from an untyped grab bag, anything could be sitting inside props.name or props.onSelect and TypeScript would never notice, to a named interface, NomineeBadgeProps, destructured directly in the parameter list. onSelect's type, (e: MouseEvent<HTMLSpanElement>) => void, tells the compiler and every caller exactly what shape of handler is expected here, not just some function.
Nothing about the JSX changed. What changed is that props.name used to be whatever anyone felt
like passing, and now it is provably a string, at the keyboard, before the component ever
renders once.
useState<T>, and when the generic actually matters
useState almost always infers its type fine on its own: useState(0) is number, useState('')
is string, no generic needed. The generic earns its keep in one very common situation: state that
starts out null or undefined but will hold something else later. useState(null) alone infers
the type null, forever, which means TypeScript will refuse to ever let that state become anything
but null. Tell it the real shape up front instead:
const [selectedHouseguest, setSelectedHouseguest] = useState<string | null>(null)Now the state starts at null, can later hold a string, and every place you read
selectedHouseguest has to handle both, honestly, because the type says both are possible.
Event handler types, the ones you will actually type
Every DOM event handler prop has a matching generic event type, parameterized by which element it is attached to. You will reach for these constantly enough that memorizing the shape beats looking it up every time:
| JSX prop | Handler type |
| --- | --- |
| onClick | (e: MouseEvent<HTMLButtonElement>) => void |
| onChange | (e: ChangeEvent<HTMLInputElement>) => void |
| onSubmit | (e: FormEvent<HTMLFormElement>) => void |
The element type in the angle brackets changes with what you actually attached the handler to,
HTMLButtonElement, HTMLInputElement, HTMLSpanElement, whatever tag owns the prop. Get it
right and e.target.value on a change handler is a known string, no guessing, no as cast to
convince the compiler your input actually has a .value.
children, typed
Any component that wraps other JSX needs a type for whatever gets passed between its tags. That
type is ReactNode, imported from react, and it covers everything JSX is willing to render:
elements, strings, numbers, arrays of those, null, all of it.
function Panel({ children }: { children: ReactNode }) {
return <div className="panel">{children}</div>
}No separate interface needed for a component that only takes children, an inline object type
does the job, same as it would for any single-property parameter.
That is the whole lesson. Now prove it in the comp below.