Menu

© 2026 Furkanul Islam

}
{
</>

Why TypeScript + React Is My Frontend Stack of Choice

Discover why TypeScript combined with React creates a powerful development experience. Learn about type safety, component patterns, and real-world benefits.

For full-stack projects and data dashboards, TypeScript with React is my go-to combination. After years of building everything from internal analytics tools to customer-facing applications, I’ve found this stack delivers the best balance of developer experience, type safety, and production reliability. In this post, I’ll explain why.

Why TypeScript? The Pain Points It Solves

Remember this JavaScript scenario:

// ❌ JavaScript: Runtime surprises
function processOrder(order) {
    return order.items.map(item => {
        return item.price * item.quantity * (1 - order.discount);
    }).reduce((sum, item) => sum + item, 0);
}

// Works fine... until it doesn't
processOrder({ items: [{ price: 10, quantity: 2 }], discount: 0.1 });  // ✓
processOrder({ items: [{ price: 10 }], discount: 0.1 });  // undefined * quantity = NaN
processOrder(null);  // TypeError: Cannot read property 'items'

With TypeScript:

// ✅ TypeScript: Compile-time errors
interface OrderItem {
    price: number;
    quantity: number;
}

interface Order {
    items: OrderItem[];
    discount: number;
}

function processOrder(order: Order): number {
    return order.items.map(item => {
        return item.price * item.quantity * (1 - order.discount);
    }).reduce((sum, item) => sum + item, 0);
}

// Caught at compile time!
processOrder({ items: [{ price: 10 }], discount: 0.1 });  // Error: missing 'quantity'
processOrder(null);  // Error: Type 'null' is not assignable to type 'Order'

The Tangible Benefits

BenefitJavaScriptTypeScript
Error detectionRuntime (production)Compile-time (development)
IDE supportBasic autocompleteFull type inference
RefactoringManual, error-proneSafe, automated
DocumentationComments (often outdated)Types (always current)
OnboardingLearn by trial and errorIntelliSense guides you

React + TypeScript: Better Together

Type-Safe Props

// ❌ JavaScript: Props are a mystery
function UserProfile(props) {
    return (
        <div>
            <h1>{props.user.name}</h1>
            <p>{props.user.email}</p>
            {props.showAge && <p>Age: {props.user.age}</p>}
        </div>
    );
}

// What props does this need? You'll only know at runtime.

// ✅ TypeScript: Props are self-documenting
interface User {
    id: string;
    name: string;
    email: string;
    age?: number;  // Optional
}

interface UserProfileProps {
    user: User;
    showAge?: boolean;  // Optional with default behavior
    onLogout?: () => void;  // Callback
}

const UserProfile: React.FC<UserProfileProps> = ({
    user,
    showAge = false,
    onLogout
}) => {
    return (
        <div>
            <h1>{user.name}</h1>
            <p>{user.email}</p>
            {showAge && user.age && <p>Age: {user.age}</p>}
            {onLogout && <button onClick={onLogout}>Logout</button>}
        </div>
    );
};

// IDE shows exactly what's needed
<UserProfile
    user={{...}}       // ← Autocomplete shows required fields
    showAge={true}     // ← Optional props shown
    onLogout={() => {}} // ← Callback signature shown
/>;

Type-Safe State

interface DashboardState {
    data: Metrics[];
    isLoading: boolean;
    error: string | null;
    lastUpdated: Date | null;
}

// TypeScript catches state mistakes
const [state, setState] = useState<DashboardState>({
    data: [],
    isLoading: false,
    error: null,
    lastUpdated: null
});

// Error: Type 'string' is not assignable to type 'Date | null'
setState({ ...state, lastUpdated: 'today' });

// Correct
setState({ ...state, lastUpdated: new Date() });

Common Patterns I Use

1. Custom Hooks with Types

// Generic fetch hook
interface UseFetchResult<T> {
    data: T | null;
    loading: boolean;
    error: Error | null;
    refetch: () => Promise<void>;
}

function useFetch<T>(
    url: string,
    options?: RequestInit
): UseFetchResult<T> {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    const fetchData = useCallback(async () => {
        try {
            setLoading(true);
            const response = await fetch(url, options);
            if (!response.ok) throw new Error(response.statusText);
            const result = await response.json();
            setData(result);
        } catch (e) {
            setError(e as Error);
        } finally {
            setLoading(false);
        }
    }, [url, options]);

    useEffect(() => {
        fetchData();
    }, [fetchData]);

    return { data, loading, error, refetch: fetchData };
}

// Usage with full type inference
interface UserData {
    id: string;
    name: string;
    email: string;
}

function UserList() {
    const { data, loading, error } = useFetch<UserData[]>('/api/users');

    if (loading) return <div>Loading...</div>;
    if (error) return <div>Error: {error.message}</div>;
    if (!data) return null;

    return (
        <ul>
            {data.map(user => (
                <li key={user.id}>{user.name} ({user.email})</li>
            ))}
        </ul>
    );
}

2. Generic Components

// Generic 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 = 'No items'
}: ListProps<T>) {
    if (items.length === 0) {
        return <p>{emptyMessage}</p>;
    }

    return (
        <ul>
            {items.map(item => (
                <li key={keyExtractor(item)}>
                    {renderItem(item)}
                </li>
            ))}
        </ul>
    );
}

// Usage
interface Product {
    id: number;
    name: string;
    price: number;
}

<List<Product>
    items={products}
    keyExtractor={(item) => item.id.toString()}
    renderItem={(item) => (
        <div>
            <h3>{item.name}</h3>
            <p>${item.price.toFixed(2)}</p>
        </div>
    )}
/>;

3. Discriminated Unions for State Machines

// Type-safe state machines
type AsyncState<T> =
    | { status: 'idle' }
    | { status: 'loading'; progress?: number }
    | { status: 'success'; data: T }
    | { status: 'error'; error: Error };

function DataFetcher<T>({ url }: { url: string }) {
    const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });

    // TypeScript enforces correct state handling
    switch (state.status) {
        case 'idle':
            return <button onClick={() => setState({ status: 'loading' })}>
                Fetch Data
            </button>;

        case 'loading':
            return (
                <div>
                    <p>Loading...</p>
                    {state.progress && <progress value={state.progress} />}
                </div>
            );

        case 'success':
            // TypeScript knows 'data' exists here
            return <pre>{JSON.stringify(state.data, null, 2)}</pre>;

        case 'error':
            // TypeScript knows 'error' exists here
            return <div className="error">{state.error.message}</div>;

        default:
            // TypeScript will error if we miss a case
            const exhaustiveCheck: never = state;
            return exhaustiveCheck;
    }
}

4. Event Handlers with Proper Types

interface FormState {
    email: string;
    password: string;
}

interface ValidationErrors {
    email?: string;
    password?: string;
}

function LoginForm() {
    const [formState, setFormState] = useState<FormState>({
        email: '',
        password: ''
    });

    const [errors, setErrors] = useState<ValidationErrors>({});

    // Typed event handler
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setFormState(prev => ({ ...prev, [name]: value }));
        // Clear error when user types
        setErrors(prev => ({ ...prev, [name]: undefined }));
    };

    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();

        // Validate
        const newErrors: ValidationErrors = {};
        if (!formState.email.includes('@')) {
            newErrors.email = 'Please enter a valid email';
        }
        if (formState.password.length < 8) {
            newErrors.password = 'Password must be at least 8 characters';
        }

        if (Object.keys(newErrors).length > 0) {
            setErrors(newErrors);
            return;
        }

        // Submit form
        console.log('Submitting:', formState);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input
                type="email"
                name="email"
                value={formState.email}
                onChange={handleChange}
            />
            {errors.email && <span className="error">{errors.email}</span>}

            <input
                type="password"
                name="password"
                value={formState.password}
                onChange={handleChange}
            />
            {errors.password && <span className="error">{errors.password}</span>}

            <button type="submit">Sign In</button>
        </form>
    );
}

Real-World Example: Data Dashboard

Here’s a complete example from a real analytics dashboard I built:

// types/analytics.ts
export interface Metric {
    id: string;
    name: string;
    value: number;
    change: number;
    trend: 'up' | 'down' | 'neutral';
}

export interface ChartDataPoint {
    date: string;
    value: number;
}

export interface DashboardFilters {
    dateRange: { start: string; end: string };
    segment?: string;
    channel?: string;
}

// components/MetricCard.tsx
interface MetricCardProps {
    metric: Metric;
    onClick?: (metricId: string) => void;
    selected?: boolean;
}

export const MetricCard: React.FC<MetricCardProps> = ({
    metric,
    onClick,
    selected = false
}) => {
    const trendIcon = metric.trend === 'up' ? '↑' : metric.trend === 'down' ? '↓' : '→';
    const trendColor = metric.trend === 'up' ? 'text-green-600' :
                       metric.trend === 'down' ? 'text-red-600' : 'text-gray-600';

    return (
        <div
            className={`metric-card ${selected ? 'selected' : ''}`}
            onClick={() => onClick?.(metric.id)}
        >
            <h3>{metric.name}</h3>
            <p className="value">{metric.value.toLocaleString()}</p>
            <p className={`change ${trendColor}`}>
                {trendIcon} {metric.change > 0 ? '+' : ''}{metric.change}%
            </p>
        </div>
    );
};

// hooks/useAnalytics.ts
interface UseAnalyticsOptions {
    autoRefetch?: boolean;
    refetchInterval?: number;
}

export function useAnalytics(
    filters: DashboardFilters,
    options: UseAnalyticsOptions = {}
) {
    const { autoRefetch = false, refetchInterval = 30000 } = options;

    const fetchMetrics = useCallback(async (): Promise<Metric[]> => {
        const response = await fetch('/api/analytics/metrics', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(filters)
        });

        if (!response.ok) {
            throw new Error('Failed to fetch metrics');
        }

        return response.json();
    }, [filters]);

    const { data, loading, error, refetch } = useFetch<Metric[]>(
        '/api/analytics/metrics',
        {
            method: 'POST',
            body: JSON.stringify(filters)
        }
    );

    // Auto-refetch
    useEffect(() => {
        if (!autoRefetch) return;

        const interval = setInterval(refetch, refetchInterval);
        return () => clearInterval(interval);
    }, [autoRefetch, refetchInterval, refetch]);

    return { metrics: data, loading, error, refetch };
}

// pages/Dashboard.tsx
export const Dashboard: React.FC = () => {
    const [filters, setFilters] = useState<DashboardFilters>({
        dateRange: {
            start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
            end: new Date().toISOString()
        }
    });

    const { metrics, loading, error, refetch } = useAnalytics(filters, {
        autoRefetch: true,
        refetchInterval: 60000  // Refetch every minute
    });

    const handleMetricClick = useCallback((metricId: string) => {
        console.log('Selected metric:', metricId);
        // Navigate to detail view, open modal, etc.
    }, []);

    if (error) {
        return (
            <div className="error-state">
                <p>Failed to load dashboard data</p>
                <button onClick={refetch}>Retry</button>
            </div>
        );
    }

    return (
        <div className="dashboard">
            <header>
                <h1>Analytics Dashboard</h1>
                <DateRangePicker
                    value={filters.dateRange}
                    onChange={(dateRange) => setFilters({ ...filters, dateRange })}
                />
            </header>

            {loading && !metrics ? (
                <DashboardSkeleton />
            ) : (
                <div className="metrics-grid">
                    {metrics?.map(metric => (
                        <MetricCard
                            key={metric.id}
                            metric={metric}
                            onClick={handleMetricClick}
                        />
                    ))}
                </div>
            )}
        </div>
    );
};

The Learning Curve: Worth It

Yes, TypeScript adds initial overhead. You’ll spend time:

  • Defining types
  • Fixing type errors
  • Learning generic patterns

But this investment pays off:

PhaseJavaScriptTypeScript
DevelopmentFastSlightly slower
DebuggingHours finding runtime bugsMinutes fixing compile errors
RefactoringFear of breaking thingsConfidence from type safety
OnboardingWeeks to learn the codebaseDays with type hints
ProductionRuntime errorsType errors caught before deploy

When TypeScript Shines

Perfect for:

  • Large codebases with multiple developers
  • Data-heavy applications (dashboards, analytics)
  • Component libraries and design systems
  • Long-term maintained projects
  • APIs with strict contracts

Maybe overkill:

  • Quick prototypes (though I still use it)
  • Simple static sites
  • One-off scripts

Getting Started

# Add TypeScript to React project
npm install typescript @types/react @types/react-dom

# For Create React App, it's built-in
npx create-react-app my-app --template typescript

# For Vite
npm create vite@latest my-app -- --template react-ts

Basic tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Key Takeaways

TypeScript + React gives you:

  1. Type safety: Catch errors before production
  2. Better DX: Autocomplete, inline docs, safe refactoring
  3. Self-documenting code: Types describe intent
  4. Confidence: Refactor without fear
  5. Team productivity: Clear contracts between components

The initial learning curve pays dividends in maintainability and developer happiness.


Questions about TypeScript or React? Reach out through the contact page or connect on LinkedIn.

MD Furkanul Islam

MD Furkanul Islam

Data Engineer & AI/ML Specialist

9+ years building intelligent data systems at scale. Passionate about bridging the gap between data engineering, AI, and robotics.