function Button(props: { children: React.ReactNode }) {
return <button>{props.children}</button>;
}
// you can use interface too
type ButtonProps = {
className: string;
children: React.ReactNode;
};
function Button(props: ButtonProps) {
return <button className={props.className}>{props.children}</button>;
}
// with destructuring
function OtherButton({ className, ...props }: ButtonProps) {
return <button className={className}>{props.children}</button>;
}
type ButtonProps = {
disabled?: boolean;
className: string;
children: React.ReactNode;
};
function Button({ disabled = true, ...props }: ButtonProps) {
return (
<button disabled={disabled} {...props}>
{props.children}
</button>
);
}
type ButtonProps = {
// accept everything React can render
children: React.ReactNode;
};
function Button(props: ButtonProps) {
return <button>{props.children}</button>;
}
import React, { ComponentProps } from "react";
//ComponentProps<"button"> : get all type/props from native button element
function Button(props: ComponentProps<"button">) {
return <button>{props.children}</button>;
}
import React, { ComponentProps } from "react";
type ButtonProps = ComponentProps<"button"> & {
variant: "primary" | "secondary";
};
function Button(props: ButtonProps) {
return <button {...props}>{props.children}</button>;
}
import React, { ComponentProps } from "react";
//remove onChange property from input with Omit<Type, Keys> and combine with new type
type InputProps = Omit<ComponentProps<"input">, "onChange"> & {
onChange: (value: string) => void;
};
function Input(props: InputProps) {
return <input {...props} />;
}
Useful when author of some external library dont export the type definition
import { ComponentProps } from "react";
import { Navbar } from "some-ui-library";
type NavBarProps = ComponentProps<typeof NavBar>;
Hover native html props in VSCode, you can copy paste the type definition
type ButtonProps = {
other?: Boolean;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
};
// ❌ Typescript already know `text` type is string
const [text, setText] = useState<string>("");
// ✅ no need to tell typescript, only work with primitive value
const [text, setText] = useState("");
type Tag = {
id: number;
value: string;
};
const [tags, setTags] = useState<Tag[]>([]);
// data : Data | undefined
const [data, setData] = useState<Data>();
// data : Data | undefined
const [data, setData] = useState<Data>(undefined);
// data : Data | null
const [data, setData] = useState<Data | null>(null);
function App(props: { id: number }) {
const handleClick = useCallback(
//⬇️ add type here
(message: string) => {
console.log("name");
},
[props.id]
);
return (
<div>
<p>{message}</p>
<button onClick={() => handleClick("hello")}>button 1</button>
<button onClick={() => handleClick("hello")}>button 2</button>
</div>
);
}
export const Component = () => {
// pass type if it doesn't have initial value
const id1 = useRef<string>();
// no need to pass type if it have initial value
const id2 = useRef("");
useEffect(() => {
id1.current = "Random value!";
}, []);
return <div></div>;
};
export const Component = () => {
// add null to initial value
// HTMLdivElement, HTMLinputElement etc.
const ref = useRef<HTMLDivElement>(null);
return <div ref={ref} />;
};
type InputProps = {
className: string;
};
const MyInput = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} className={props.className} />;
});
// add displayName if you are using function expression, so its has a name in React Devtool
MyInput.displayName = "MyInput";
function App() {
const input = React.useRef<HTMLInputElement>(null);
useEffect(() => {
// focus to input element on first render
if (input.current) {
input.current.focus();
}
}, []);
return <MyInput className="input-style" ref={input} />;
}
export const Component = () => {
const ref1 = useRef<string>(null);
// if you pass null to initial value
// this not allowed to change directly
ref1.current = "Hello";
const ref2 = useRef<string>();
// if initial value is undefined this is allowed to change (mutable)
ref2.current = "Hello";
return null;
};
You can use Discriminated Unions for reducer actions. Don't forget to define the return type of reducer, otherwise TypeScript will infer it.
import { useReducer } from "react";
const initialState = { count: 0 };
type ACTIONTYPE =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: string };
function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - Number(action.payload) };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "decrement", payload: "5" })}>
-
</button>
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
</>
);
}
import { createContext, useState } from "react";
type ThemeContextType = "light" | "dark";
const ThemeContext = createContext<ThemeContextType | null>(null);
//if you have proper default value, you dont need specify null
//const ThemeContext = createContext<ThemeContextType>("light");
const useTheme = () => {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error(
"useTheme has to be used within <ThemeContext.Provider>"
);
}
return theme;
};
const App = () => {
const [theme, setTheme] = useState<ThemeContextType>("light");
return (
<ThemeContext.Provider value={theme}>
<MyComponent />
</ThemeContext.Provider>
);
};
const SomeComponent = () => {
// since the value has been checked inside useTheme, no need checking null value
const theme = useTheme();
return <p>current theme: {theme}.</p>;
};
Imagine a Button
component that renders a <button>
element, but with your fancy button styles. If want to render the Button component as an a other element we might have an API like:
<Button variantColor="primary" href="https://blog.makerx.com.au/" as="a">
Click me
</Button>
This looks nice, but its not work realy well with typescript. here the alternative using radix-ui/react-slot
// Button.tsx
import { Slot } from '@radix-ui/react-slot'
type ButtonProps = React.ComponentPropsWithoutRef<'button'> & {
variantColor: 'primary' | 'secondary' | 'danger'
asChild?: boolean
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, forwardedRef) => {
const { variantColor, asChild, ...buttonProps } = props
const Component = (asChild ? Slot : 'button') as 'button'
return (
<Component
{...buttonProps}
ref={forwardedRef}
className={clsx(
// ...
)}
/>
)
}
// App.tsx
function App() {
return (
<div>
{/* asChild must be true */}
<Button variantColor="primary" asChild>
{/* render button component as link */}
<a href="https://google.com">About</a>
</Button>
</div>
)
}
interface
s are different from type
s in TypeScript, but they can be used for very similar things as far as common React uses cases are concerned. Here's a helpful rule of thumb:
-
Always use
interface
for public API's definition when authoring a library or 3rd party ambient type definitions. -
Consider using
type
for your React Component Props and State, because it is more constrained.
Types are useful for union types (e.g. type MyType = TypeA | TypeB
) whereas Interfaces are better for declaring dictionary shapes and then implementing
or extending
them.