PropTypes in React: Silently Ignored Since 19, and What Replaced It
React 19 removed propTypes for function components. Zero declarations across 92 component files here, one copy still in node_modules, and the three patterns that took over.
PropTypes is React's old runtime prop checker: you attach a propTypes object to a component and React warns in the console when a prop arrives with the wrong type. React 19 removed it for function components — the field is now silently ignored — and points you at TypeScript instead. This codebase runs React 19.2.4 and declares propTypes zero times across 92 component files.
It still has prop-types@15.8.1 installed, which turns out to be the most instructive thing about it. Below is what actually replaced runtime prop validation in a real App Router codebase, what the replacement does not cover, and the one place here that still validates shapes by hand.
What changed, and when
| React version | Status of PropTypes |
|---|---|
| ≤ 15.4 | React.PropTypes shipped in React core |
| 15.5 | Moved out to the standalone prop-types package; core access deprecated |
| 16–18 | propTypes on function components still checked in development |
| 19 | Removed for function components — the field is ignored, with no warning |
The React 19 release notes are blunt about the replacement: migrate to TypeScript or another type-checking solution. defaultProps went the same way for function components, with ES6 default parameters named as the substitute (class components keep both, since there is no ES6 alternative there).
"Silently ignored" is the part that bites. A codebase upgrading to 19 with propTypes still attached does not get a build error or a console warning — it just stops being checked. If PropTypes was your only prop contract, you lost it on upgrade day and nothing said so.
The copy still in node_modules
A fresh npm ci here installs prop-types, and the dependency tree says why:
$ npm ls prop-types
asoc-landing-page@0.1.0
`-- eslint-config-next@16.2.9
`-- eslint-plugin-react@7.37.5
`-- prop-types@15.8.1
It arrives through the linter, not the app. eslint-plugin-react depends on it to implement rules like react/prop-types, and eslint-config-next pulls that plugin in. Nothing under src/ imports it:
$ grep -rn "prop-types\|PropTypes" src/
# (no output)
So the package is present, unused by the bundle, and harmless — a useful reminder that "it's in node_modules" and "it's in my app" are different claims. If you are auditing a React 19 migration, grep your own source for propTypes, not your lockfile.
What replaced it: three patterns, one of them unusual
The interesting half is not "we use TypeScript." It is which TypeScript pattern each layer uses, because the choice is load-bearing in an Atomic Design tree.
1. The data file owns the type (19 of 41 molecules).
This is the pattern a PropTypes shape cannot express. A molecule that renders one item of a content array does not declare its own prop type — it imports the type of that item and uses it as the whole prop list:
// src/components/molecules/FeatureCard.tsx
import type { FeatureItem } from "@/data/features";
export default function FeatureCard({ icon, title, description }: FeatureItem) {
FeatureItem is declared next to the content it describes:
// src/data/features.tsx
export type FeatureItem = {
icon: ReactNode;
title: string;
description: string;
};
export const featureCards: FeatureItem[] = [ /* ... */ ];
Nineteen of the 41 molecules here do exactly this — TechStackCard takes TechStackItem, DocsSidebarLink takes DocLink, PluginCard takes PluginItem, FaqItem takes FaqEntry. The import is import type, which is a project rule rather than a stylistic preference: molecules may know the shape of section content but never import the content itself, so a card component cannot quietly start reading the array it was supposed to receive by prop.
The payoff is that there is exactly one definition of what a feature is. Add a required field to FeatureItem and both the data file and the component fail to compile until they agree. A propTypes block would have been a second, hand-maintained copy of that shape sitting in the component — and a copy that only disagrees at runtime, in development, if someone happens to render the broken case.
2. An inline type literal when the component wraps an element.
The Button atom types its props in the signature and intersects the native anchor attributes, so call sites can pass aria-label or rel without the atom re-declaring HTML:
}: {
variant?: ButtonVariant;
href?: string;
external?: boolean;
disabled?: boolean;
className?: string;
children: ReactNode;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) {
PropTypes has no intersection. Covering the same surface meant either enumerating every anchor attribute or giving up and accepting anything. (Passing props in React takes that atom apart in detail, including the ...rest forwarding and the default parameters that replaced defaultProps.)
3. A named interface, exactly once.
Across 92 component files there is one *Props interface: AvatarGroupProps, with 8 fields, in src/components/molecules/AvatarGroup.tsx. It earns the name because it is the one shared molecule whose props are presentation knobs rather than a content row — two sections render it with different sizes, colors and heading classes, and each field carries a doc comment explaining which section needed it.
That is the implicit rule worth stealing: reuse the data type when the component renders a data row; name an interface when the props are a control panel. Only one component here is the second kind.
What types do not do, and where this repo still checks by hand
TypeScript erases. Every type above is gone at runtime, so none of it validates data arriving from outside the build — which is the one argument for PropTypes that was always real.
This codebase has exactly one such boundary that matters: the LemonSqueezy purchase webhook. It is validated by hand, from unknown outward:
// src/lib/lemonsqueezy/webhook.ts
let payload: unknown;
try {
payload = JSON.parse(rawBody);
} catch {
return { status: 400, body: "invalid payload" };
}
const metaCandidate = isRecord(payload) ? payload.meta : undefined;
const dataCandidate = isRecord(payload) ? payload.data : undefined;
// ...
if (
!isRecord(metaCandidate) ||
typeof eventName !== "string" ||
!isRecord(attributesCandidate) ||
lsOrderId === null
) {
return { status: 202, body: "ignored" };
}
No schema library — there is no zod or equivalent in this dependency list — and, more to the point, no type assertion. JSON.parse returns unknown and every field is narrowed before use, because the alternative (as WebhookPayload) is a lie the compiler believes.
Note where this check lives: in a route handler, on a payload that never becomes React props. PropTypes would not have covered it even in React 18. Runtime prop validation guards the boundary between your own components; untrusted input needs guarding at the edge it enters, which is a different place in the stack.
Build-time catches a class PropTypes never saw
The checking this repo actually relies on runs in npm run build, which type-checks every component. A real example from this codebase's conventions: inline SVGs must keep JSX casing (strokeWidth, clipPath, linearGradient). Write <clippath> in lowercase and it compiles fine in dev and fails next build, because lowercase element names are treated as unknown intrinsic elements.
That is a JSX type error, not a prop-value error. PropTypes inspects the values handed to a component you declared; it has no opinion about whether the elements inside it exist. Most of what breaks a component tree in practice — a renamed field, a missing required prop, a typo'd element, a wrong union member — is in that category.
| Checker | Runs | Catches | Misses |
|---|---|---|---|
| PropTypes | Dev runtime, on render | Wrong prop types on rendered paths | Unrendered paths, JSX errors, anything outside props |
| TypeScript | next build / editor | Every prop, element and union across all files | Anything at runtime — API payloads, JSON.parse, user input |
Hand narrowing from unknown | Runtime, at the edge | Untrusted external data | Nothing it is pointed at; it is explicit by construction |
The honest reading is that TypeScript replaces PropTypes completely for the job PropTypes did, and that neither ever covered the edge. You need the third row regardless.
When PropTypes is still the right call
It is deprecated, not useless:
- A plain-JavaScript React app with no build-time type checking. PropTypes is still the only prop contract available, and the
prop-typespackage is maintained. - A published component library with JavaScript consumers. Types help the TS users; PropTypes warns the others.
- A React 18 or older codebase. Nothing has broken yet; plan the migration with the upgrade.
If you are on React 19 with function components, there is no third option: the field does nothing. Delete it with the migration or replace it with types, but do not leave it in place believing it still checks.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
propTypes warnings vanished after upgrading | React 19 ignores the field on function components | Move the contract into TypeScript |
prop-types in the lockfile after removing all usage | Transitive dependency of eslint-plugin-react | Expected — grep src/, not the lockfile |
react/prop-types lint errors | A React ESLint preset has the rule on | eslint-config-next already sets 'react/prop-types': 'off'; a second preset after it may re-enable it |
| Types pass, production still crashes on bad data | Types erase at runtime | Narrow from unknown at the boundary, as the webhook does |
| Prop type drifts from the data file | The component declared its own copy | Import the data file's type and use it as the prop type |
defaultProps ignored | Removed for function components in 19 | Use default parameters in the destructure |
Frequently asked questions
Is PropTypes deprecated?
For function components it is past deprecation — React 19 removed the behaviour, so the field is inert. The prop-types package itself is still published and still works where you call it directly.
Do I need both PropTypes and TypeScript? No. They overlap almost entirely, and the duplicate shape is a maintenance cost that only pays off in a library shipping to untyped consumers.
What replaces defaultProps?
Default parameters in the destructured signature: function Button({ variant = "primary" }). That is what the atom here does, and it needs no React API at all.
How do I type props without a Props interface?
Two ways, both in use here: an inline type literal in the signature, or the data file's own item type imported with import type. React with TypeScript covers the trade-offs across a whole component tree.
Templates with typed props end to end
Every component in these landing sets types its props the way described above — data types imported from the content file, no propTypes, and a build that type-checks the whole tree: ASoc Ignite, ASoc Iris and ASoc Keystone.
Browse the sets: Next.js landing page templates and Tailwind landing page templates. For the component boundaries these types enforce, read Atomic Design in Next.js.
