Skip to main content
ASoc
Tutorial

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.

The ASoc Team9 min read

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 versionStatus of PropTypes
≤ 15.4React.PropTypes shipped in React core
15.5Moved out to the standalone prop-types package; core access deprecated
16–18propTypes on function components still checked in development
19Removed 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.

CheckerRunsCatchesMisses
PropTypesDev runtime, on renderWrong prop types on rendered pathsUnrendered paths, JSX errors, anything outside props
TypeScriptnext build / editorEvery prop, element and union across all filesAnything at runtime — API payloads, JSON.parse, user input
Hand narrowing from unknownRuntime, at the edgeUntrusted external dataNothing 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-types package 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

SymptomCauseFix
propTypes warnings vanished after upgradingReact 19 ignores the field on function componentsMove the contract into TypeScript
prop-types in the lockfile after removing all usageTransitive dependency of eslint-plugin-reactExpected — grep src/, not the lockfile
react/prop-types lint errorsA React ESLint preset has the rule oneslint-config-next already sets 'react/prop-types': 'off'; a second preset after it may re-enable it
Types pass, production still crashes on bad dataTypes erase at runtimeNarrow from unknown at the boundary, as the webhook does
Prop type drifts from the data fileThe component declared its own copyImport the data file's type and use it as the prop type
defaultProps ignoredRemoved for function components in 19Use 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.

Keep reading

Tutorial10 min read

React Bundle Size: The Framework Floor vs. the Part You Control

Bundle size is two numbers, not one. Measured here: a 195.2 KiB gzipped floor identical on every page, and the 68 KiB auth stack that used to sit inside it.

Read more