01 - React + TypeScript Patterns ​
Why This Matters for Glomopay ​
They build checkout SDKs and merchant dashboards — type safety is critical in fintech where a wrong type can mean wrong money movement.
1. Component Typing ​
Basic Props ​
tsx
// Always use interface for component props (extendable, better error messages)
interface PaymentFormProps {
amount: number;
currency: string;
onSubmit: (data: PaymentData) => Promise<void>;
onCancel?: () => void; // optional
}
function PaymentForm({ amount, currency, onSubmit, onCancel }: PaymentFormProps) {
// ...
}Children Patterns ​
tsx
// Explicit children
interface LayoutProps {
children: React.ReactNode; // most flexible — accepts anything renderable
}
// Render prop
interface DataLoaderProps<T> {
url: string;
children: (data: T, isLoading: boolean) => React.ReactNode;
}Event Handlers ​
tsx
// Don't do: (e: any) => ...
// Do:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}
// For callbacks passed as props — use the domain type, not the event
interface Props {
onAmountChange: (amount: number) => void; // NOT (e: ChangeEvent) => void
}2. Discriminated Unions (Very Important) ​
This is the #1 TypeScript pattern for React. Used for state machines, API responses, and multi-step flows (like checkout).
tsx
// Payment status — each state carries only its relevant data
type PaymentState =
| { status: 'idle' }
| { status: 'processing'; startedAt: number }
| { status: 'success'; transactionId: string; receipt: Receipt }
| { status: 'error'; error: string; retryable: boolean };
// Usage — TypeScript narrows the type inside each branch
function PaymentStatus({ state }: { state: PaymentState }) {
switch (state.status) {
case 'idle':
return <p>Ready to pay</p>;
case 'processing':
return <Spinner startedAt={state.startedAt} />;
case 'success':
return <Receipt id={state.transactionId} data={state.receipt} />;
case 'error':
return (
<div>
<p>{state.error}</p>
{state.retryable && <button>Retry</button>}
</div>
);
}
}Why it matters ​
- Impossible states become unrepresentable
- No
if (state.transactionId)guesswork - Compiler catches missing cases with
exhaustive check
tsx
// Exhaustive check helper
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
// Add to switch default — compiler errors if you miss a case
default: return assertNever(state);3. Generics ​
Generic Components ​
tsx
// Reusable list component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage ?? 'No items'}</p>;
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage — T is inferred
<List
items={transactions}
renderItem={(tx) => <TransactionRow tx={tx} />}
keyExtractor={(tx) => tx.id}
/>Generic Hooks ​
tsx
// API fetch hook with generic return type
function useApi<T>(url: string): {
data: T | null;
isLoading: boolean;
error: string | null;
} {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, [url]);
return { data, isLoading, error };
}
// Usage
const { data: payment } = useApi<Payment>('/api/payments/123');
// payment is Payment | null — fully typed4. Utility Types (Know These) ​
tsx
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'merchant' | 'user';
createdAt: Date;
}
// Pick — only specific fields
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit — everything except
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
// Partial — all fields optional (useful for update payloads)
type UpdateUserInput = Partial<Omit<User, 'id'>>;
// Required — make all fields required
type CompleteUser = Required<User>;
// Record — typed dictionary
type TransactionsByStatus = Record<PaymentStatus, Transaction[]>;
// Extract / Exclude — filter union types
type SuccessOrError = Extract<PaymentState, { status: 'success' | 'error' }>;5. Hooks Typing ​
tsx
// useRef — specify the element type
const inputRef = useRef<HTMLInputElement>(null);
const timerRef = useRef<number | null>(null); // for mutable refs
// useReducer — covered in state management notes
// useState with complex types
const [filters, setFilters] = useState<FilterState>({ page: 1, query: '' });
// forwardRef (React 19 — ref is a regular prop now)
// React 19:
interface InputProps {
label: string;
ref?: React.Ref<HTMLInputElement>;
}
function Input({ label, ref }: InputProps) {
return <input ref={ref} aria-label={label} />;
}
// Pre-React 19:
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} aria-label={props.label} />;
});6. as const and Literal Types ​
tsx
// as const for config objects
const PAYMENT_METHODS = ['card', 'upi', 'netbanking', 'wallet'] as const;
type PaymentMethod = (typeof PAYMENT_METHODS)[number];
// type is 'card' | 'upi' | 'netbanking' | 'wallet' — not string[]
// Useful for steps in a checkout flow
const CHECKOUT_STEPS = ['cart', 'payment', 'review', 'confirmation'] as const;
type CheckoutStep = (typeof CHECKOUT_STEPS)[number];Quick Revision ​
- Use
interfacefor props,typefor unions and computed types - Discriminated unions for any multi-state scenario
- Generics for reusable components and hooks
- Utility types to derive types (don't duplicate)
as constfor literal arrays/objects- Never use
any— useunknownif the type is truly unknown