I just implemented a custom hook for this which plays well with Typescript.
Custom hook:
const usePropsWithDefaults = <
P extends Record<string, unknown>, I extends P, D extends P
>(incomingProps: I, defaultProps: D) => {
// We need a ref of incomingProps so we can compare previous props to incoming props
const inRef = React.useRef<P>(incomingProps);
// We need a ref of result because we might want to return exactly the same object if props object has not changed
const outRef = React.useRef<P>({ ...defaultProps, incomingProps });
// props object has changed so we can return a new object which is a spread of defaultProps and incomingProps
if (inRef.current !== incomingProps) {
inRef.current = incomingProps;
outRef.current = { ...defaultProps, ...incomingProps };
return outRef.current as I & D;
}
// one or more props have changed.
Object.assign(outRef.current, incomingProps);
return outRef.current as I & D;
}
Implementation:
export type Props = {
one: string;
two?: number;
three: boolean;
four?: string;
};
export const defaultProps = {
one: '',
two: 2,
three: false,
} satisfies Partial<Props>;
const myComponent = (incomingProps: Props) => {
const props = usePropsWithDefaults(incomingProps, defaultProps);
// ...
}