GitHub Repo: pkellner/pluralsight-using-typescript-with-react
Framework: Next.js · React (concurrent rendering, Suspense, Server Components, Server Functions)
Table of Contents
- 1. Introduction to TypeScript in a React Application
- 2. Converting a JavaScript Application to TypeScript
- 3. Runtime Type Safety — Zod and React Server Components
- Reference Diagrams
- Reference Tables
1. Introduction to TypeScript in a React Application
Why TypeScript?
React is 100% JavaScript, and JavaScript is a dynamically typed language: a variable’s type is determined implicitly at assignment. This can lead to unexpected behavior.
JavaScript type coercion example:
// JavaScript — surprising behavior
let result = 5 + "5"; // result: "55" (string), not 10 (number)
console.log(typeof result); // "string"
Ambiguous comparison example:
function whichIsLarger(a, b) {
return a > b ? `${a} is larger` : `${a} is smaller`;
}
whichIsLarger(101, 1001); // "101 is smaller" ✓
whichIsLarger("101", "1001"); // "1001 is smaller than 101" ✗ (lexicographic sort)
whichIsLarger(101, "1001"); // unpredictable per ECMAScript spec §7.2.13
TypeScript solves these problems by detecting errors at compile time, before code is even executed. Editors like VS Code leverage these types to provide IntelliSense (auto-completion, real-time error detection).
TypeScript Primitive Types
TypeScript defines three fundamental primitive types:
| Type | Description | Example |
|---|---|---|
string | Text | "hello world" |
number | Number (integer or decimal) | 42, 3.14 |
boolean | True or false | true, false |
let message: string = "Hello, TypeScript!";
let itemCount: number = 42;
let isVisible: boolean = true;
Type Annotations and Inference
TypeScript supports two complementary approaches:
1. Explicit annotation:
let name: string = "hello";
name = 1; // ✗ Error: Type 'number' is not assignable to type 'string'
2. Implicit inference (recommended for simple cases):
let name = "hello"; // TypeScript automatically infers: string
name = 1; // ✗ Same error — TypeScript inferred the string type
Best practice: Implicit inference simplifies JavaScript → TypeScript migration. It is preferred when the type is obvious. Use explicit annotations for function parameters and component props.
The tsconfig.json file with "strict": true is essential to get the full benefit of TypeScript in React projects:
{
"compilerOptions": {
"strict": true,
"jsx": "preserve",
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"moduleResolution": "bundler"
}
}
The Three TypeScript Patterns for React
These three patterns cover the vast majority of cases encountered in React development:
mindmap
root((TypeScript<br/>React Patterns))
Pattern 1
Function Parameters
Custom Hooks
Typing inputs/outputs
Pattern 2
Component Properties
Typed props
Interfaces / Types
Pattern 3
Generic Types
useState generic
useReducer
createContext
2. Converting a JavaScript Application to TypeScript
Pattern 1: Function Parameters (custom hooks)
This pattern applies to any JavaScript function, but especially to React custom hooks.
Step 1 — Rename the file: use-speaker-sort-and-filter.js → use-speaker-sort-and-filter.ts
Before (JavaScript):
function useSpeakerSortAndFilter(speakerList, speakingSaturday, speakingSunday, searchQuery) {
return speakerList.filter(speaker =>
(speakingSaturday ? speaker.sat : true) &&
(speakingSunday ? speaker.sun : true) &&
(speaker.firstName + " " + speaker.lastName)
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
}
After (TypeScript):
function useSpeakerSortAndFilter(
speakerList: { sat: boolean; sun: boolean; firstName: string; lastName: string }[],
speakingSaturday: boolean,
speakingSunday: boolean,
searchQuery: string
): { sat: boolean; sun: boolean; firstName: string; lastName: string }[] {
return speakerList.filter(speaker =>
(speakingSaturday ? speaker.sat : true) &&
(speakingSunday ? speaker.sun : true) &&
(speaker.firstName + " " + speaker.lastName)
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
}
Note:
"strict": trueintsconfig.jsonforbids implicitanytypes. Every parameter must be explicitly typed.
Types and Interfaces
To simplify and reuse complex type definitions, TypeScript provides two syntaxes: type and interface.
With type:
type Speaker = {
id: number;
firstName: string;
lastName: string;
sat: boolean;
sun: boolean;
imageUrl: string;
userBioShort: string;
company: string;
favorite: boolean;
};
With interface (functionally equivalent, extensible):
interface Speaker {
id: number;
firstName: string;
lastName: string;
sat: boolean;
sun: boolean;
imageUrl: string;
userBioShort: string;
company: string;
favorite: boolean;
}
// Interface extension is possible:
interface ExtendedSpeaker extends Speaker {
twitterHandle: string;
}
Simplified custom hook using the Speaker type:
function useSpeakerSortAndFilter(
speakerList: Speaker[],
speakingSaturday: boolean,
speakingSunday: boolean,
searchQuery: string
): Speaker[] {
return speakerList.filter(speaker =>
(speakingSaturday ? speaker.sat : true) &&
(speakingSunday ? speaker.sun : true) &&
`${speaker.firstName} ${speaker.lastName}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
}
| Criterion | type | interface |
|---|---|---|
| Syntax | type Foo = { ... } | interface Foo { ... } |
| Extension | Intersection (&) | extends |
| Declaration merging | ✗ Not supported | ✓ Supported |
| Union types | ✓ Supports A | B | ✗ Not directly supported |
| Recommendation | Complex types, unions | Public contracts, extensibility |
Pattern 2: Component Properties (props)
This pattern concerns typing the props received by React components.
Rename: speaker-detail.jsx → speaker-detail.tsx
Before (JavaScript):
function SpeakerDetail({ speakerRec }) {
return (
<div>
<img src={speakerRec.imageUrl} alt={speakerRec.firstName} />
<h3>{speakerRec.firstName} {speakerRec.lastName}</h3>
<p>{speakerRec.userBioShort}</p>
<span>{speakerRec.company}</span>
</div>
);
}
After (TypeScript):
type SpeakerDetailProps = {
speakerRec: Speaker;
};
function SpeakerDetail({ speakerRec }: SpeakerDetailProps) {
return (
<div>
<img src={speakerRec.imageUrl} alt={speakerRec.firstName} />
<h3>{speakerRec.firstName} {speakerRec.lastName}</h3>
<p>{speakerRec.userBioShort}</p>
<span>{speakerRec.company}</span>
</div>
);
}
With React.FC (alternative):
import { FC } from 'react';
type SpeakerDetailProps = {
speakerRec: Speaker;
onFavoriteToggle?: (id: number) => void; // optional prop
};
const SpeakerDetail: FC<SpeakerDetailProps> = ({ speakerRec, onFavoriteToggle }) => {
return (
<div onClick={() => onFavoriteToggle?.(speakerRec.id)}>
<h3>{speakerRec.firstName} {speakerRec.lastName}</h3>
</div>
);
};
Typing children:
import { ReactNode } from 'react';
type LayoutProps = {
children: ReactNode;
title: string;
};
function Layout({ children, title }: LayoutProps) {
return (
<main>
<h1>{title}</h1>
{children}
</main>
);
}
Pattern 3: Generic Types
Generics allow reusable functions to work with different types while maintaining type safety. In React, they are pervasive with useState, useReducer, and createContext.
Generic useState
// Without initialization — union type required
const [darkTheme, setDarkTheme] = useState<boolean | undefined>();
// darkTheme: boolean | undefined
// setDarkTheme: Dispatch<SetStateAction<boolean | undefined>>
// With initialization — implicit inference (recommended)
const [darkTheme, setDarkTheme] = useState(false);
// darkTheme: boolean (automatically inferred)
// Array of objects
const [speakers, setSpeakers] = useState<Speaker[]>([]);
// Null initial value
const [errorMsg, setErrorMsg] = useState<string | null>(null);
useContext with createContext
Complex approach (when initialization is ambiguous):
type ThemeContextType = {
darkTheme: boolean | undefined;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType>({
darkTheme: undefined,
toggleTheme: () => {},
});
type ThemeProviderProps = {
children: ReactNode;
};
function ThemeProvider({ children }: ThemeProviderProps) {
const [darkTheme, setDarkTheme] = useState<boolean | undefined>();
function toggleTheme() {
setDarkTheme(prev => !prev);
}
return (
<ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Simplified approach (with initialization — implicit inference):
// Initializing to false eliminates the need to handle `undefined`
function ThemeProvider({ children }: ThemeProviderProps) {
const [darkTheme, setDarkTheme] = useState(false); // boolean inferred
function toggleTheme() {
setDarkTheme(prev => !prev);
}
// createContext automatically infers the type from useState(false)
const ThemeContext = createContext({
darkTheme: false,
toggleTheme: () => {},
});
return (
<ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Key lesson: Correctly initializing values with
useState(anduseReducer) radically simplifies TypeScript typing. Implicit inference often eliminates the need for union types withundefined.
Migrating ThemeProvider to TypeScript
Renaming theme-context.jsx → theme-context.tsx immediately generates three TypeScript errors:
- Type of the
ThemeProvidercomponent → apply the Component Properties pattern - Type of the
setDarkThemesetter → apply the Generic Types pattern onuseState - Type of
createContext→ declare an explicit type for the context
import { createContext, useState, ReactNode } from 'react';
type ThemeContextType = {
darkTheme: boolean;
toggleTheme: () => void;
};
export const ThemeContext = createContext<ThemeContextType>({
darkTheme: false,
toggleTheme: () => {},
});
type ThemeProviderProps = {
children: ReactNode;
};
export function ThemeProvider({ children }: ThemeProviderProps) {
const [darkTheme, setDarkTheme] = useState(false);
function toggleTheme() {
setDarkTheme(prev => !prev);
}
return (
<ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
useReducer and the TypeScript Union Type
useReducer is preferable to useState when multiple interdependent states need to be managed together. TypeScript and the Union type make its usage safer and more readable.
useState vs useReducer comparison:
// === useState — simple, for independent states ===
function CounterWithState() {
const [count, setCount] = useState<number>(0);
return (
<div>
<button onClick={() => setCount(c => c - 1)}>−</button>
<span>{count}</span>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
);
}
// === useReducer — for interdependent states or complex logic ===
type CounterState = { count: number };
type CounterAction = { type: "increment" } | { type: "decrement" };
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "decrement": return { count: state.count - 1 };
}
}
function CounterWithReducer() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<button onClick={() => dispatch({ type: "decrement" })}>−</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
</div>
);
}
Discriminated Union with useReducer
The TypeScript discriminated union allows defining multiple object types sharing a discriminating property (here actionType), with different payloads depending on the action type.
Defining the discriminating type:
// /types/reducer-action-type.ts
export type ReducerActionType =
| { actionType: "TOGGLE_FAVORITE_OPTIMISTIC"; payload: boolean }
| { actionType: "TOGGLE_FAVORITE_REVERT"; payload: boolean }
| { actionType: "ERROR"; payload: string }
| { actionType: "CLEAR_ERROR" }; // no payload
Typed State and Reducer:
type State = {
speaker: Speaker;
error: string | null;
};
function speakerReducer(state: State, action: ReducerActionType): State {
switch (action.actionType) {
case "TOGGLE_FAVORITE_OPTIMISTIC":
return {
...state,
speaker: { ...state.speaker, favorite: action.payload },
};
case "TOGGLE_FAVORITE_REVERT":
return {
...state,
speaker: { ...state.speaker, favorite: action.payload },
};
case "ERROR":
return { ...state, error: action.payload };
case "CLEAR_ERROR":
return { ...state, error: null };
}
}
Component with optimistic UI:
type SpeakerFavoriteToggleProps = {
speakerRec: Speaker;
};
function SpeakerFavoriteToggle({ speakerRec }: SpeakerFavoriteToggleProps) {
const [state, dispatch] = useReducer(speakerReducer, {
speaker: speakerRec,
error: null,
});
async function handleFavoriteClick() {
const newFavorite = !state.speaker.favorite;
// Optimistic UI: immediate update
dispatch({ actionType: "TOGGLE_FAVORITE_OPTIMISTIC", payload: newFavorite });
try {
await speakerFavoriteToggleAction(state.speaker);
} catch {
// Rollback on server error
dispatch({ actionType: "TOGGLE_FAVORITE_REVERT", payload: !newFavorite });
dispatch({ actionType: "ERROR", payload: "Failed to update favorite" });
}
}
return (
<button onClick={handleFavoriteClick}>
{state.error && <span>{state.error}</span>}
<HeartIcon filled={state.speaker.favorite} />
</button>
);
}
useRef and DOM Manipulation
useRef allows direct access to DOM nodes. TypeScript enforces the reference type and ensures consistency between initialization and usage.
Two main use cases:
- Store data without triggering a re-render (render counters, timers)
- Directly access a DOM node (auto-centering, focus, scroll)
import { useRef, useCallback } from 'react';
// Case 1: Render counter (does not cause re-render)
function RenderCounter() {
const renderCount = useRef<number>(0);
renderCount.current += 1;
return <div>Render count: {renderCount.current}</div>;
}
// Case 2: Reference to a DOM element for auto-scrolling
function SpeakerRow({ speaker }: { speaker: Speaker }) {
const rowRef = useRef<HTMLDivElement>(null);
const handleClick = useCallback(() => {
if (rowRef.current) {
rowRef.current.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, []);
return (
<div ref={rowRef} onClick={handleClick}>
<h3>{speaker.firstName} {speaker.lastName}</h3>
</div>
);
}
useRef types by use case:
// Mutable reference (internal data — not tied to the DOM)
const renderCount = useRef<number>(0);
// renderCount.current is freely modifiable
// DOM reference — initialized to null (React will assign value on mount)
const divRef = useRef<HTMLDivElement>(null);
// divRef.current is readonly from TypeScript's perspective
// DOM reference — HTMLInputElement for an input field
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // optional chaining for safety
3. Runtime Type Safety — Zod and React Server Components
Why Runtime Validation?
TypeScript is a transpiler: it produces JavaScript and no longer exists at runtime. It guarantees type safety at code-writing time, but not when data arrives over a network.
| Layer | TypeScript | Zod |
|---|---|---|
| Check timing | Compile time (edit) | Runtime |
| Network data | ✗ Not verified | ✓ Verified and parsed |
| Form data | ✗ Not verified | ✓ Verified and parsed |
| VS Code IntelliSense | ✓ | ✓ (via z.infer) |
| TypeScript output | Static types | Types + runtime validation |
Typical cases where Zod is necessary:
- Data received from a REST API (GET speakers)
- Data sent to a Server Function (React Server Functions)
- Passing data between React Server Components and Client Components (crossing the network boundary)
Creating a Zod Schema
Installation:
npm install zod
Schema definition and TypeScript type (DRY — Don’t Repeat Yourself):
// /types/speaker.ts
import { z } from "zod";
// Zod schema — defines the runtime validation rules
export const speakerSchema = z.object({
id: z.number().int().positive(),
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
sat: z.boolean(),
sun: z.boolean(),
imageUrl: z.string().url(),
userBioShort: z.string().max(500),
company: z.string(),
favorite: z.boolean(),
});
// TypeScript type inferred from the Zod schema — avoids duplication
export type Speaker = z.infer<typeof speakerSchema>;
// Equivalent to:
// type Speaker = {
// id: number;
// firstName: string;
// ...
// }
Zod validation methods:
// parse — throws an exception if data is invalid
const validSpeaker = speakerSchema.parse(rawData);
// safeParse — returns { success, data } or { success: false, error }
const result = speakerSchema.safeParse(rawData);
if (result.success) {
console.log(result.data); // typed Speaker
} else {
console.error(result.error.issues); // error details
}
// For an array of speakers
const validSpeakers = speakerSchema.array().parse(rawDataArray);
Server-side Validation (React Server Component)
// /app/speakers-page.tsx — React Server Component (runs on Node.js)
import { speakerSchema } from "@/types/speaker";
async function SpeakersPage() {
// Fetch data from the source
const rawData = await loadSpeakersData();
// Runtime validation with Zod — throws an error if data is invalid
const speakersData = speakerSchema.array().parse(rawData);
// speakersData is now guaranteed as valid Speaker[]
return (
<SpeakersDataProvider initialData={speakersData}>
<SpeakersList />
</SpeakersDataProvider>
);
}
Client-side Validation (React Client Component)
// /app/speakers-list.tsx — React Client Component (runs in the browser)
"use client";
import { speakerSchema } from "@/types/speaker";
import { useSpeakerSortAndFilter } from "@/hooks/use-speaker-sort-and-filter";
function SpeakersList() {
const { speakerList } = useContext(SpeakersDataContext);
// Runtime validation in the browser — same Zod schema on both client and server
const validatedSpeakers = speakerSchema.array().parse(speakerList);
// Data has crossed a network boundary — validation is essential
const filteredSpeakers = useSpeakerSortAndFilter(
validatedSpeakers,
speakingSaturday,
speakingSunday,
searchQuery
);
return (
<div>
{filteredSpeakers.map(speaker => (
<SpeakerDetail key={speaker.id} speakerRec={speaker} />
))}
</div>
);
}
Validation in a React Server Function
// /actions/speaker-favorite-toggle-action.ts
// Rename: .js → .ts
import { Speaker, speakerSchema } from "@/types/speaker";
export async function speakerFavoriteToggleAction(speakerRec: Speaker) {
// Runtime validation at the Server Function entry point
// Data comes from the browser — network boundary crossed
const validatedSpeaker = speakerSchema.parse(speakerRec);
// If invalid, a ZodError is automatically thrown
// Update in the database
await updateSpeakerFavorite(validatedSpeaker.id, validatedSpeaker.favorite);
return { success: true };
}
Reference Diagrams
TypeScript Type System for React
graph TD
A[TypeScript Types] --> B[Primitives]
A --> C[Composite Types]
A --> D[Generics]
A --> E[Union Types]
B --> B1[string]
B --> B2[number]
B --> B3[boolean]
C --> C1["type / interface"]
C1 --> C2[Speaker]
C1 --> C3[Props]
C1 --> C4[State]
D --> D1["useState<T>"]
D --> D2["useReducer<S,A>"]
D --> D3["createContext<T>"]
D --> D4["useRef<T>"]
E --> E1["boolean | undefined"]
E --> E2["string | null"]
E --> E3[Discriminated Union]
E3 --> E3A["{ type: 'A'; payload: boolean }"]
E3 --> E3B["{ type: 'B'; payload: string }"]
E3 --> E3C["{ type: 'C' }"]
C2 --> F[React Component Props]
F --> F1["SpeakerDetailProps"]
F --> F2["ThemeProviderProps"]
F --> F3["LayoutProps - children: ReactNode"]
JS → TS Conversion Flow
flowchart LR
A["File .jsx / .js"] -->|Rename| B["File .tsx / .ts"]
B --> C{TypeScript errors?}
C -->|Function parameters| D["Apply Pattern 1\nFunction Parameters"]
C -->|Component props| E["Apply Pattern 2\nComponent Properties"]
C -->|useState / useReducer\ncreateContext| F["Apply Pattern 3\nGeneric Types"]
D --> G[Define type for each parameter]
E --> H["Create TypeXxxProps\nAssign to props"]
F --> I["useState<T>\ncreateContext<T>"]
G --> J[✓ No TypeScript errors]
H --> J
I --> J
Zod Architecture in Next.js
sequenceDiagram
participant Browser as Browser<br/>(Client Component)
participant Server as Node.js<br/>(Server Component)
participant DB as Database
Server->>DB: loadSpeakersData()
DB-->>Server: rawData (unverified)
Server->>Server: speakerSchema.array().parse(rawData)
Note over Server: Zod validation ✓<br/>Throws ZodError if invalid
Server-->>Browser: Validated Speaker[] (serialized)
Browser->>Browser: speakerSchema.array().parse(speakerList)
Note over Browser: Re-validation Zod ✓<br/>Network boundary crossed
Browser->>Server: speakerFavoriteToggleAction(speaker)
Server->>Server: speakerSchema.parse(speakerRec)
Note over Server: Entry validation ✓
Server->>DB: updateSpeakerFavorite()
DB-->>Server: OK
Server-->>Browser: { success: true }
Reference Tables
Common React Types
| React Type | Description | Typical usage |
|---|---|---|
ReactNode | Anything React can render (JSX, string, null…) | Component children |
ReactElement | A specific JSX element (result of React.createElement) | Specific component return type |
FC<Props> | Function Component with typed props | const Comp: FC<Props> = () => ... |
Dispatch<A> | Dispatch function from useReducer | Passing dispatch as a prop |
SetStateAction<S> | Value or function passed to a useState setter | Typing a useState setter |
RefObject<T> | Ref created with useRef(null) — tied to a DOM node | useRef<HTMLDivElement>(null) |
MutableRefObject<T> | Mutable ref — internal data (not DOM) | useRef<number>(0) |
CSSProperties | React inline style object | style: CSSProperties |
ComponentProps<T> | Infer props from an existing component | type Btn = ComponentProps<'button'> |
Typed React Events
| Event | TypeScript Type | Access example |
|---|---|---|
| Click (button) | React.MouseEvent<HTMLButtonElement> | e.currentTarget |
| Click (div) | React.MouseEvent<HTMLDivElement> | e.preventDefault() |
| Change (input) | React.ChangeEvent<HTMLInputElement> | e.target.value |
| Change (select) | React.ChangeEvent<HTMLSelectElement> | e.target.value |
| Submit (form) | React.FormEvent<HTMLFormElement> | e.preventDefault() |
| KeyDown | React.KeyboardEvent<HTMLInputElement> | e.key, e.code |
| Focus | React.FocusEvent<HTMLInputElement> | e.target, e.relatedTarget |
| DragStart | React.DragEvent<HTMLDivElement> | e.dataTransfer |
Concrete examples:
// Handler for a search input
function SearchBar() {
const [query, setQuery] = useState("");
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setQuery(e.target.value);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Escape") setQuery("");
}
return (
<input
type="text"
value={query}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Search..."
/>
);
}
// Handler for a form
function LoginForm() {
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
console.log(formData.get("email"));
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<button type="submit">Sign In</button>
</form>
);
}
Common TypeScript Utilities
| Utility | Description | Example |
|---|---|---|
Partial<T> | All properties of T become optional | Partial<Speaker> — for partial updates |
Required<T> | All properties of T become required | Required<Speaker> |
Readonly<T> | All properties of T become read-only | Readonly<Speaker[]> |
Pick<T, K> | Select certain properties from T | Pick<Speaker, 'id' | 'firstName'> |
Omit<T, K> | Exclude certain properties from T | Omit<Speaker, 'imageUrl'> |
Record<K, V> | Object with keys K and values V | Record<string, Speaker> |
NonNullable<T> | Exclude null and undefined from T | NonNullable<string | null> |
ReturnType<F> | Return type of a function | ReturnType<typeof useSpeakerSortAndFilter> |
Parameters<F> | Parameter types of a function | Parameters<typeof fetch> |
z.infer<typeof S> | Infer the TypeScript type from a Zod schema | z.infer<typeof speakerSchema> |
React Hooks and Their TypeScript Signatures
| Hook | TypeScript Signature | Notes |
|---|---|---|
useState | useState<S>(initialState: S): [S, Dispatch<SetStateAction<S>>] | Inference possible if initial value provided |
useReducer | useReducer<R extends Reducer<any,any>>(reducer, initialState): [state, dispatch] | State and Action typed via the reducer |
useRef (DOM) | useRef<T>(null): RefObject<T> | T = DOM element type |
useRef (mutable) | useRef<T>(initialValue: T): MutableRefObject<T> | For storing values without re-render |
useContext | useContext<T>(context: Context<T>): T | Type inferred from createContext<T> |
useCallback | useCallback<T extends Function>(callback: T, deps): T | Type inferred from the callback |
useMemo | useMemo<T>(factory: () => T, deps): T | Type inferred from the factory |
useEffect | useEffect(effect: () => void | (() => void), deps?): void | No generic — always void |
useTransition | useTransition(): [boolean, TransitionStartFunction] | isPending: boolean |
Summary and Best Practices
flowchart TD
A[React Project] --> B{New project?}
B -->|Yes| C["npx create-next-app@latest\n→ Answer 'Yes' to TypeScript"]
B -->|No - Migration| D[Rename files .js → .ts\n.jsx → .tsx]
D --> E[Apply the 3 patterns\none file at a time]
E --> F{Network data?}
F -->|Yes| G[Add Zod\nnpm install zod]
F -->|No| H[TypeScript alone is sufficient]
G --> I[Create speakerSchema\nExport type via z.infer]
I --> J[Validate: Server Component\nClient Component\nServer Function]
C --> K[✓ Type-safe application]
H --> K
J --> K
JS → TS Migration Checklist:
-
tsconfig.jsonwith"strict": true - Rename each file
.jsx→.tsx/.js→.ts(one at a time) - Apply Pattern 1 (function parameters) to custom hooks
- Apply Pattern 2 (component properties) to components
- Apply Pattern 3 (generics) to
useState,useReducer,createContext - Prefer initializing state values (avoids unnecessary
undefined) - Use
typefor complex unions,interfacefor extensibility - For network data: install Zod and create schemas
- Validate with Zod at both ends of every network boundary
Resources:
- react.dev/learn/typescript — React + TypeScript migration guide
- zod.dev — Zod documentation
- Course GitHub Repo
Search Terms
typescript · react · frontend · development · type · types · validation · component · pattern · server · zod · application · function · generic · hooks · reference · runtime · union · usereducer