Extending Native HTML Props in React/TS

When you’re building a custom React component that needs to act just like a standard HTML element, you often want it to accept all the same props. Manually listing every single possible property (like onClick, id, aria-label, etc.) is super tedious and a huge waste of time.

Luckily, TypeScript and React give us a neat utility to handle this, React.ComponentProps<T>.

This utility lets you grab the prop types from any native HTML tag or another React component.

For example, if you’re making a custom <Button> component and you want it to accept everything a native <button> can, you just pass the tag name as a string literal to the utility:

import React from "react";

// This type now includes ALL props a standard <button> takes (onClick, disabled, etc.)
type 
type ButtonProps = React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement> & {
    isLoading?: boolean;
}
ButtonProps
= React.type ComponentProps<T extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>> = T extends React.JSXElementConstructor<infer Props> ? Props : T extends keyof React.JSX.IntrinsicElements ? React.JSX.IntrinsicElements[T] : {}
Used to retrieve the props a component accepts. Can either be passed a string, indicating a DOM element (e.g. 'div', 'span', etc.) or the type of a React component. It's usually better to use {@link ComponentPropsWithRef } or {@link ComponentPropsWithoutRef } instead of this type, as they let you be explicit about whether or not to include the `ref` prop.
@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/componentprops/ React TypeScript Cheatsheet}@example```tsx // Retrieves the props an 'input' element accepts type InputProps = React.ComponentProps<'input'>; ```@example```tsx const MyComponent = (props: { foo: number, bar: string }) => <div />; // Retrieves the props 'MyComponent' accepts type MyComponentProps = React.ComponentProps<typeof MyComponent>; ```
ComponentProps
<"button"> & {
isLoading?: boolean | undefinedisLoading?: boolean; // Our custom component }; const const Button: ({ isLoading, children, ...props }: ButtonProps) => React.JSX.ElementButton = ({ isLoading: boolean | undefinedisLoading, children: React.ReactNodechildren, ...
props: {
    ref?: React.Ref<HTMLButtonElement> | undefined;
    key?: React.Key | null | undefined;
    disabled?: boolean | undefined;
    form?: string | undefined;
    formAction?: string | ((formData: FormData) => void | Promise<void>) | React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS[keyof React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS] | undefined;
    ... 283 more ...;
    onTransitionStartCapture?: React.TransitionEventHandler<...> | undefined;
}
props
}:
type ButtonProps = React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement> & {
    isLoading?: boolean;
}
ButtonProps
) => {
return ( <JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button ButtonHTMLAttributes<T>.disabled?: boolean | undefineddisabled={isLoading: boolean | undefinedisLoading} {...
props: {
    ref?: React.Ref<HTMLButtonElement> | undefined;
    key?: React.Key | null | undefined;
    disabled?: boolean | undefined;
    form?: string | undefined;
    formAction?: string | ((formData: FormData) => void | Promise<void>) | React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS[keyof React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_FORM_ACTIONS] | undefined;
    ... 283 more ...;
    onTransitionStartCapture?: React.TransitionEventHandler<...> | undefined;
}
props
}>
{isLoading: boolean | undefinedisLoading ? "Loading..." : children: React.ReactNodechildren} </JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> ); }; // Now you can use it like a native button, passing any standard HTML attribute: const const Form: () => React.JSX.ElementForm = () => { return <const Button: ({ isLoading, children, ...props }: ButtonProps) => React.JSX.ElementButton ButtonHTMLAttributes<T>.type?: "button" | "submit" | "reset" | undefinedtype="submit" DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() => var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
@see[source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
console
.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.
@sincev0.1.100
log
("click")} />;
};

You can do this for any standard tag. For a <div>, it would look like this:

import React from "react";

type type CustomDivProps = React.ClassAttributes<HTMLDivElement> & React.HTMLAttributes<HTMLDivElement>CustomDivProps = React.type ComponentProps<T extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>> = T extends React.JSXElementConstructor<infer Props> ? Props : T extends keyof React.JSX.IntrinsicElements ? React.JSX.IntrinsicElements[T] : {}
Used to retrieve the props a component accepts. Can either be passed a string, indicating a DOM element (e.g. 'div', 'span', etc.) or the type of a React component. It's usually better to use {@link ComponentPropsWithRef } or {@link ComponentPropsWithoutRef } instead of this type, as they let you be explicit about whether or not to include the `ref` prop.
@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/componentprops/ React TypeScript Cheatsheet}@example```tsx // Retrieves the props an 'input' element accepts type InputProps = React.ComponentProps<'input'>; ```@example```tsx const MyComponent = (props: { foo: number, bar: string }) => <div />; // Retrieves the props 'MyComponent' accepts type MyComponentProps = React.ComponentProps<typeof MyComponent>; ```
ComponentProps
<"div">;
// CustomDivProps now includes props like onClick, style, role, etc.

Sometimes you might want your component to accept all standard props except one. You might want to remove a prop because you’re handling it differently or want to ensure users don’t override your component’s internal logic.

This is where TypeScript’s built-in Omit<T, K> utility type comes in handy. It takes an existing type (T) and lets you remove specific properties (K).

Let’s say we want to always set the className ourselves and prevent the user from passing their own.

import React from "react";

// We use Omit to remove 'className' from the standard button props
type 
type ButtonProps = {
    form?: string | undefined | undefined;
    slot?: string | undefined | undefined;
    style?: React.CSSProperties | undefined;
    title?: string | undefined | undefined;
    ref?: React.Ref<HTMLButtonElement> | undefined;
    key?: React.Key | null | undefined;
    disabled?: boolean | undefined | undefined;
    formAction?: string | ((formData: FormData) => void | Promise<void>) | undefined;
    formEncType?: string | undefined | undefined;
    formMethod?: string | undefined | undefined;
    formNoValidate?: boolean | undefined | undefined;
    formTarget?: string | undefined | undefined;
    name?: string | undefined | undefined;
    type?: "submit" | "reset" | "button" | undefined | undefined;
    ... 274 more ...;
    onTransitionStartCapture?: React.TransitionEventHandler<...> | undefined;
}
ButtonProps
= type Omit<T, K extends keyof any> = { [P in Exclude<keyof T, K>]: T[P]; }
Construct a type with the properties of T except for those in type K.
Omit
<React.type ComponentProps<T extends keyof React.JSX.IntrinsicElements | React.JSXElementConstructor<any>> = T extends React.JSXElementConstructor<infer Props> ? Props : T extends keyof React.JSX.IntrinsicElements ? React.JSX.IntrinsicElements[T] : {}
Used to retrieve the props a component accepts. Can either be passed a string, indicating a DOM element (e.g. 'div', 'span', etc.) or the type of a React component. It's usually better to use {@link ComponentPropsWithRef } or {@link ComponentPropsWithoutRef } instead of this type, as they let you be explicit about whether or not to include the `ref` prop.
@see{@link https://react-typescript-cheatsheet.netlify.app/docs/react-types/componentprops/ React TypeScript Cheatsheet}@example```tsx // Retrieves the props an 'input' element accepts type InputProps = React.ComponentProps<'input'>; ```@example```tsx const MyComponent = (props: { foo: number, bar: string }) => <div />; // Retrieves the props 'MyComponent' accepts type MyComponentProps = React.ComponentProps<typeof MyComponent>; ```
ComponentProps
<"button">, "className">;
const const Button: ({ children, ...props }: ButtonProps) => React.JSX.ElementButton = ({ children: React.ReactNodechildren, ...
props: {
    form?: string | undefined | undefined;
    slot?: string | undefined | undefined;
    style?: React.CSSProperties | undefined;
    title?: string | undefined | undefined;
    ref?: React.Ref<HTMLButtonElement> | undefined;
    key?: React.Key | null | undefined;
    disabled?: boolean | undefined | undefined;
    formAction?: string | ((formData: FormData) => void | Promise<void>) | undefined;
    formEncType?: string | undefined | undefined;
    formMethod?: string | undefined | undefined;
    formNoValidate?: boolean | undefined | undefined;
    formTarget?: string | undefined | undefined;
    name?: string | undefined | undefined;
    type?: "submit" | "reset" | "button" | undefined | undefined;
    ... 273 more ...;
    onTransitionStartCapture?: React.TransitionEventHandler<...> | undefined;
}
props
}:
type ButtonProps = {
    form?: string | undefined | undefined;
    slot?: string | undefined | undefined;
    style?: React.CSSProperties | undefined;
    title?: string | undefined | undefined;
    ref?: React.Ref<HTMLButtonElement> | undefined;
    key?: React.Key | null | undefined;
    disabled?: boolean | undefined | undefined;
    formAction?: string | ((formData: FormData) => void | Promise<void>) | undefined;
    formEncType?: string | undefined | undefined;
    formMethod?: string | undefined | undefined;
    formNoValidate?: boolean | undefined | undefined;
    formTarget?: string | undefined | undefined;
    name?: string | undefined | undefined;
    type?: "submit" | "reset" | "button" | undefined | undefined;
    ... 274 more ...;
    onTransitionStartCapture?: React.TransitionEventHandler<...> | undefined;
}
ButtonProps
) => {
// We apply our desired class name here, which is now guaranteed not to be overridden return ( <JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button HTMLAttributes<HTMLButtonElement>.className?: string | undefinedclassName="bg-red-500 font-bold" {...
props: {
    form?: string | undefined | undefined;
    slot?: string | undefined | undefined;
    style?: React.CSSProperties | undefined;
    title?: string | undefined | undefined;
    ref?: React.Ref<HTMLButtonElement> | undefined;
    key?: React.Key | null | undefined;
    disabled?: boolean | undefined | undefined;
    formAction?: string | ((formData: FormData) => void | Promise<void>) | undefined;
    formEncType?: string | undefined | undefined;
    formMethod?: string | undefined | undefined;
    formNoValidate?: boolean | undefined | undefined;
    formTarget?: string | undefined | undefined;
    name?: string | undefined | undefined;
    type?: "submit" | "reset" | "button" | undefined | undefined;
    ... 273 more ...;
    onTransitionStartCapture?: React.TransitionEventHandler<...> | undefined;
}
props
}>
{children: React.ReactNodechildren} </JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button> ); }; // Now, if a user tries to pass 'className', TypeScript throws an error. const const Form: () => React.JSX.ElementForm = () => { return <const Button: ({ children, ...props }: ButtonProps) => React.JSX.ElementButton className="bg-black">Submit</const Button: ({ children, ...props }: ButtonProps) => React.JSX.ElementButton>;
Type '{ children: string; className: string; }' is not assignable to type 'IntrinsicAttributes & ButtonProps'. Property 'className' does not exist on type 'IntrinsicAttributes & ButtonProps'.
};

If we didn’t use Omit in this case, the className="bg-black" passed by the user would completely override our default className="bg-red-500 font-bold".

420 words

© 2023. All rights reserved.