TypeScript for Component Props
Props form the connection between a component and every parent that renders it. In JavaScript, that connection is established by names and runtime values. TypeScript adds a static description that tools can check before the browser runs the component.
Consider a small lesson card.
function LessonCard({ title, minutes }) {
return <p>{title}, {minutes} min</p>;
}The function assumes title is text and minutes is a number. JavaScript permits a caller to violate both assumptions.
<LessonCard title={42} minutes="ten" />React can still receive these values. The visible result may be odd, and a later operation such as title.toUpperCase() will fail. TypeScript lets the file state the intended contract next to the component.
TypeScript Checks Source, Not User Input
Rename the file to LessonCard.tsx and describe the props.
type LessonCardProps = {
title: string;
minutes: number;
};
function LessonCard({ title, minutes }: LessonCardProps) {
return <p>{title}, {minutes} min</p>;
}The annotation after the destructured parameter tells TypeScript what the props object should contain. A typed caller now receives an error for the wrong values.
TypeScript performs this analysis before runtime. The type is erased from the JavaScript sent to the browser. It does not validate JSON from a server, form input, local storage, or a value cast from an untyped source.
Static types check code relationships. Data crossing a runtime trust point still needs runtime validation.
This distinction prevents a serious misunderstanding. A User type does not force an API response to contain a valid user. It describes what the program assumes after any required validation has occurred.
Why the Extension Changes
Use .ts for TypeScript without JSX and .tsx for TypeScript containing JSX.
src/
lesson-data.ts
LessonCard.tsx
App.tsx
main.tsxThe x tells the TypeScript parser and build tools to accept JSX syntax. Renaming App.jsx to App.ts while leaving JSX inside it produces parsing errors.
TypeScript can infer many local values without annotations.
const title = "TypeScript for Component Props";
const minutes = 12;The inferred types are string and number. Do not annotate every local variable only to repeat what the initializer already proves. Type the boundaries where values enter a component, function, or data model.
Start from the React TypeScript Template
For a new project, Vite supplies React TypeScript templates.
npm create vite@latest reactbook-typed -- --template react-tsThe SWC variant is also available through react-swc-ts. Both produce a TypeScript React starting point. The regular react-ts template matches the project used here.
For the current JavaScript project, install TypeScript and React's type declarations before renaming files.
npm install --save-dev typescript @types/react @types/react-domThen add TypeScript configuration. Copying the current Vite template configuration is safer than pasting an old one from a previous toolchain. Compiler options change as TypeScript and bundlers change.
React's runtime package and its TypeScript declarations have separate package names. react runs in the application. @types/react supplies static declarations to TypeScript and editors.
Vite Transpiles but Does Not Type-Check
Vite can remove TypeScript syntax and transform .tsx modules. It does not run the TypeScript type checker as part of its normal transformation pipeline.
This file can therefore appear in a development page while a separate type check reports a problem.
const total: number = "six";The type annotation disappears during transformation. The runtime value remains the string "six".
Add a script that invokes the compiler.
{
"scripts": {
"typecheck": "tsc -b --pretty",
"build": "tsc -b && vite build"
}
}Run the check directly while editing.
npm run typecheckThe current Vite React TypeScript template uses project references and separate configuration for application and Node-side tool files. tsc -b builds or checks the referenced projects.
A successful Vite page load is not a successful type check. Keep an explicit TypeScript command in local and automated review steps.
Required Props
Properties without ? are required.
type LessonCardProps = {
title: string;
minutes: number;
};This use is incomplete.
<LessonCard title="TypeScript for Props" />TypeScript reports that minutes is missing. The report points to the caller, which is where the incomplete element was created.
Types make a component's demands searchable. A reader can open LessonCardProps without studying every line of the function body.
Optional Props and Defaults
Add ? when omission is supported by the component's behavior.
type LessonCardProps = {
title: string;
minutes?: number;
};Inside the component, minutes has the type number | undefined.
function LessonCard({ title, minutes }: LessonCardProps) {
return <p>{title}, {minutes} min</p>;
}That output can display undefined min. The type is not asking TypeScript to invent a fallback. Add component behavior for the omitted case.
function LessonCard({ title, minutes = 10 }: LessonCardProps) {
return <p>{title}, {minutes} min</p>;
}Or omit the text.
{minutes !== undefined && <span>{minutes} min</span>}Make a prop optional only when the component has a defined, useful result without it. Optional syntax should describe behavior, not silence a caller error.
Restrict Known String Values
The card has three supported statuses.
type LessonStatus = "not-started" | "reading" | "complete";
type LessonCardProps = {
title: string;
status: LessonStatus;
};This is a union of string literal types. A misspelling is rejected.
<LessonCard title="Props" status="completed" />The allowed value is "complete", not "completed".
The runtime is still plain strings. The union gives the editor completion and lets the component account for the supported set.
const labels: Record<LessonStatus, string> = {
"not-started": "Not started",
reading: "Reading",
complete: "Complete",
};If another status is added to LessonStatus, TypeScript requires another label entry.
Type Object Props at the Data Level
Repeated lesson fields can have one named data type.
type Lesson = {
id: string;
title: string;
minutes: number;
complete: boolean;
};The component can receive the object.
type LessonCardProps = {
lesson: Lesson;
};
function LessonCard({ lesson }: LessonCardProps) {
return <h2>{lesson.title}</h2>;
}A list receives an array.
type LessonListProps = {
lessons: Lesson[];
};Lesson[] means an array whose items meet the Lesson type. It does not require a particular number of items.
Keep domain data types separate from component prop types. Lesson describes one lesson. LessonCardProps describes what one component accepts. They can change for different reasons.
Type Callback Props by Their Inputs and Result
A callback prop can report event intent to a parent.
type LessonCardProps = {
lesson: Lesson;
onOpen: (lessonId: string) => void;
};The function accepts one string and returns no meaningful value to the caller.
function LessonCard({ lesson, onOpen }: LessonCardProps) {
return (
<button type="button" onClick={() => onOpen(lesson.id)}>
{lesson.title}
</button>
);
}Type the component-level event by its meaning, not by copying the browser event when the parent does not need that event.
onOpen: (lessonId: string) => void;This keeps the DOM click inside LessonCard. The parent receives the lesson ID it needs.
Let JSX Infer DOM Event Types
An inline handler receives contextual type information.
<input onChange={(event) => setQuery(event.target.value)} />TypeScript infers that this handler receives an input event. Hover over event in the editor to inspect the inferred type.
For a named handler, import the event type with a type-only import.
import type { ChangeEvent } from "react";
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setQuery(event.target.value);
}import type states that the import is used only by the type checker. It does not create a runtime import in emitted JavaScript.
Do not begin with any merely to remove the editor error.
function handleChange(event: any) {
setQuery(event.target.value);
}any disables checking for operations that flow through it. Inference or a specific event type preserves useful feedback.
Type children When the Component Accepts It
React does not add a children prop to every custom prop type automatically.
import type { ReactNode } from "react";
type PanelProps = {
title: string;
children: ReactNode;
};ReactNode describes values React can accept as children, including elements, strings, numbers, empty values, and arrays of nodes.
function Panel({ title, children }: PanelProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}Use a narrower type when the component truly requires one specific child contract. Do not narrow it only because the first caller happens to pass one element.
type and interface
Both forms can describe object props.
type LessonCardProps = {
lesson: Lesson;
};interface LessonCardProps {
lesson: Lesson;
}For this application, either works. Type aliases also express unions directly, which makes one consistent type style convenient. Large codebases may adopt a convention based on extension and declaration-merging needs.
Do not turn the choice into a blocker. The quality of the property contract and runtime behavior is more useful than switching equivalent syntax across files.
Avoid Assertions as Repairs
An assertion tells TypeScript to accept the programmer's claim.
const lesson = lessons.find((item) => item.id === selectedId) as Lesson;find can return undefined. The assertion removes that fact from the type checker without changing runtime behavior.
Handle the absent case.
const lesson = lessons.find((item) => item.id === selectedId);
if (!lesson) {
return <p>Select an available lesson.</p>;
}Now the check proves that lesson exists for the remaining code.
as can suppress a useful warning. Use it when you possess information TypeScript cannot express, not when the runtime case has been ignored.
Migrate One Component Edge at a Time
For this milestone, convert a small leaf component before the whole application.
- Install the TypeScript and React type packages.
- Add current Vite TypeScript configuration.
- Rename
LessonCard.jsxtoLessonCard.tsx. - Add
LessonandLessonCardPropstypes. - Repair callers reported by
npm run typecheck. - Continue upward through
LessonList,App, andmain.
TypeScript allows JavaScript and TypeScript files to coexist when configuration permits it. A measured migration keeps the application running and makes each new error local to a small change.
The Contract Review
Review each prop type against runtime behavior.
- Required props correspond to values the component cannot omit.
- Optional props have a defined result when omitted.
- Callback inputs describe component intent.
- Server and storage values are validated before being treated as typed data.
npm run typecheckruns independently of the development server.
TypeScript is most helpful when the types state true contracts. A large number of annotations cannot repair an inaccurate contract.
Source Notes
- Vite documentation, TypeScript
- TypeScript Handbook, Everyday Types
- React documentation, Using TypeScript
- React TypeScript reference, Typing Hooks