Using TypeScript with React is a must in 2025. And with AI tools running rampant, it is even more important to know how to properly use TypeScript in your React project.
The AI often generates slop implementation because of its vast slopy training data and with time it will get trained on the very slop it generates.
With that note, today I share with you 5 ways to use TypeScript in your React project:
1. Discriminated Union Props with Intersection
// Bad ❌
type ButtonProps = {
variant: "label" | "no-label";
label?: string;
colorCss: string;
};
// Good ✅
type ButtonProps = ({variant: "no-label"} | {variant: "label"; label: string}) & {colorCss: string};
function Button(props: ButtonProps) {
if (props.variant === "no-label") return <button>No title</button>;
return <button>{props.title}</button>;
};
2. Discriminated Union in useState
// Bad ❌
export function UserList() {
const [status, setStatus] = useState({
state: "loading",
error: new Error(),
});
async function fetchUsers() {}
useEffect(() => {
setStatus((prev) => ({ ...prev, state: "loading" }));
try {
fetchUsers();
setStatus((prev) => ({ ...prev, state: "loaded" }));
} catch (err) {
if (err instanceof Error) {
setStatus({ state: "error", error: err });
}
}
}, []);
{
status.state === "loading" ? <Loading /> : null;
}
{
status.state === "error" ? <ShowError error={status?.error} /> : null;
}
{
status.state === "loaded" ? <Content /> : null;
}
}
// Good ✅
export function UserList() {
type Status =
| { state: "loading" }
| { state: "loaded" }
| { state: "error"; error: Error };
const [status, setStatus] = useState<Status>({
state: "loading",
});
async function fetchUsers() {}
useEffect(() => {
setStatus({ state: "loading" });
try {
fetchUsers();
setStatus({ state: "loaded" });
} catch (err) {
if (err instanceof Error) {
setStatus({ state: "error", error: err });
}
}
}, []);
{
status.state === "loading" ? <Loading /> : null;
}
{
status.state === "error" ? <ShowError error={status?.error} /> : null;
}
{
status.state === "loaded" ? <Content /> : null;
}
}
3. Extracting Keys and Values as Type from an Object
const RESPONSE_CODES = { 200: "Ok", 201: "Created", 202: "Accepted", 204: "No Content", 301: "Moved Permanently", 302: "Found", 304: "Not Modified", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 409: "Conflict", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout" } as const;
type ResponseCodes = typeof RESPONSE_CODES;
type StatusCode = keyof ResponseCodes;
type StatusMessage = ResponseCodes[StatusCode];
4. Reusable components using ComponentProps
// Bad ❌
interface InputFieldProps {
value: string;
};
// Good ✅
import { ComponentProps } from "react";
interface InputFieldProps extends ComponentProps<"input"> {
value: string;
};
function InputField({value, ...rest}: InputFieldProps) {
return <input value={value} {...rest} />;
};
5. Autocomplete with satisfies + ComponentProps
import { ComponentProps, PropsWithChildren } from "react";
type ButtonVariants = Record<string, ComponentProps<"button"> & {"data-testid": string}>
// Bad ❌
const buttonVariants: ButtonVariants = {
submit: {
type: "submit",
className: "bg-green-300",
"data-testid": "submit-btn"
},
next: {
type: "button",
className: "bg-blue-300",
"data-testid": "next-btn"
},
reset: {
type: "reset",
className: "bg-orange-300",
"data-testid": "reset-btn"
},
delete: {
type: "button",
className: "bg-red-300",
"data-testid": "delete-btn"
}
};
// Good ✅
const buttonVariants = {
// ...
delete: {
type: "button",
className: "bg-red-300",
"data-testid": "delete-btn"
}
} satisfies ButtonVariants;
type ButtonProps = PropsWithChildren & {variant: keyof typeof buttonVariants};
function Button({children, ...rest}: ButtonProps) {
return <button {...buttonVariants[rest.variant]}>{children}</button>
};
// Now variant will infer the types (delete, submit, next, reset) on autocomplete
<Button variant="delete">Delete record</Button>
Top comments (0)