Skip to main content

🔷 TypeScript Essentials

The TypeScript patterns you'll use every day in React Native

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Type component props and children correctly
  • Use TypeScript with useState, useRef, and other hooks
  • Type event handlers and callbacks
  • Work with React Native's built-in types
  • Apply common TypeScript patterns in mobile development

⏱️ Estimated Time: 35-45 minutes

📑 In This Lesson

Why TypeScript in React Native

TypeScript isn't just a nice-to-have in React Native — it's the recommended default. When you create an Expo project, it comes with TypeScript pre-configured. Here's why that matters:

Why TypeScript Matters for Mobile 🐛 Catch Bugs Early Errors at compile time, not runtime crashes 💡 Better IntelliSense Autocomplete props, methods, and more 📖 Self-Documenting Types explain what your code expects 🔄 Safer Refactoring Change code confidently 🤝 Team Collaboration Clear contracts between code

TypeScript provides safety nets that are especially valuable in mobile development

📱 Mobile-Specific Benefit

In mobile apps, runtime crashes are worse than on web — users might uninstall your app! TypeScript catches many errors before they reach users' devices.

TypeScript Refresher

If you're rusty on TypeScript basics, here's a quick refresher of the essentials:

// Basic types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let items: string[] = ["a", "b", "c"];

// Object types
type User = {
  id: number;
  name: string;
  email?: string;  // Optional property
};

// Interface (similar to type, can be extended)
interface Product {
  id: number;
  title: string;
  price: number;
}

// Union types
type Status = "loading" | "success" | "error";

// Function types
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Arrow function with types
const add = (a: number, b: number): number => a + b;

// Generics
function getFirst<T>(items: T[]): T | undefined {
  return items[0];
}

💡 Type vs Interface

In React Native, you can use either. The convention is:

  • type — For props, unions, and simpler shapes
  • interface — When you need to extend or implement

We'll use type for props in this course, as it's more common in the React community.

Typing Props

Props are the most common thing you'll type in React Native. Let's look at the patterns you'll use constantly.

Basic Props

import { Text, View } from 'react-native';

// Define the props type
type GreetingProps = {
  name: string;
  age: number;
};

// Use it in the component
function Greeting({ name, age }: GreetingProps) {
  return (
    <View>
      <Text>Hello, {name}!</Text>
      <Text>You are {age} years old.</Text>
    </View>
  );
}

// Usage - TypeScript ensures correct props
<Greeting name="Sarah" age={28} />  // ✅ Works
<Greeting name="Sarah" />           // ❌ Error: age is required
<Greeting name={123} age={28} />    // ❌ Error: name must be string

Optional Props

type ButtonProps = {
  title: string;
  onPress: () => void;
  variant?: "primary" | "secondary";  // Optional with union type
  disabled?: boolean;                  // Optional boolean
};

function Button({ 
  title, 
  onPress, 
  variant = "primary",  // Default value
  disabled = false 
}: ButtonProps) {
  return (
    <Pressable onPress={onPress} disabled={disabled}>
      <Text>{title}</Text>
    </Pressable>
  );
}

// All valid:
<Button title="Submit" onPress={() => {}} />
<Button title="Cancel" onPress={() => {}} variant="secondary" />
<Button title="Save" onPress={() => {}} disabled />

Children Props

import { ReactNode } from 'react';
import { View, ViewStyle } from 'react-native';

type CardProps = {
  children: ReactNode;  // Any valid React content
  style?: ViewStyle;
};

function Card({ children, style }: CardProps) {
  return (
    <View style={[styles.card, style]}>
      {children}
    </View>
  );
}

// Usage
<Card>
  <Text>I'm inside a card!</Text>
</Card>

<Card style={{ marginTop: 20 }}>
  <Text>Title</Text>
  <Text>Description</Text>
</Card>

Props with Functions

type SearchInputProps = {
  value: string;
  onChangeText: (text: string) => void;
  onSubmit?: () => void;
  placeholder?: string;
};

function SearchInput({ 
  value, 
  onChangeText, 
  onSubmit,
  placeholder = "Search..." 
}: SearchInputProps) {
  return (
    <TextInput
      value={value}
      onChangeText={onChangeText}
      onSubmitEditing={onSubmit}
      placeholder={placeholder}
    />
  );
}

Extending Native Component Props

Often you want to create a wrapper that accepts all the props of a native component plus your own:

import { Pressable, PressableProps, Text, StyleSheet } from 'react-native';

// Extend PressableProps and add our own
type CustomButtonProps = PressableProps & {
  title: string;
  variant?: 'primary' | 'secondary';
};

function CustomButton({ 
  title, 
  variant = 'primary', 
  style,
  ...rest  // All other Pressable props
}: CustomButtonProps) {
  return (
    <Pressable 
      style={[
        styles.button, 
        variant === 'secondary' && styles.secondary,
        style
      ]}
      {...rest}
    >
      <Text style={styles.text}>{title}</Text>
    </Pressable>
  );
}

// Now you get all Pressable props + title + variant
<CustomButton 
  title="Press Me" 
  variant="secondary"
  onPress={() => console.log('Pressed!')}
  disabled={isLoading}
  accessibilityLabel="Custom button"
/>

Typing Hooks

Hooks are already typed in React, but you often need to provide type parameters for full type safety.

useState

import { useState } from 'react';

// Inferred types (simple cases)
const [count, setCount] = useState(0);           // number
const [name, setName] = useState('');            // string
const [isOpen, setIsOpen] = useState(false);     // boolean

// Explicit types (when initial value doesn't tell the whole story)
const [user, setUser] = useState<User | null>(null);

type User = {
  id: string;
  name: string;
  email: string;
};

// Now user is typed as User | null
// setUser expects User | null

// Array state
const [items, setItems] = useState<string[]>([]);
const [todos, setTodos] = useState<Todo[]>([]);

// Union state
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');

useRef

import { useRef } from 'react';
import { TextInput, View } from 'react-native';

// Ref to a React Native component
const inputRef = useRef<TextInput>(null);

// Usage
<TextInput ref={inputRef} />

// Later: focus the input
inputRef.current?.focus();

// Ref for mutable values (not DOM elements)
const timerRef = useRef<NodeJS.Timeout | null>(null);

// Usage
timerRef.current = setTimeout(() => {
  console.log('Timer fired!');
}, 1000);

// Cleanup
if (timerRef.current) {
  clearTimeout(timerRef.current);
}

💡 Ref Types Tip

For component refs, use the component type directly: useRef<TextInput>(null)

For mutable values, use the value type: useRef<number>(0)

useEffect

import { useEffect, useState } from 'react';

type User = {
  id: string;
  name: string;
};

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    // Async function inside useEffect
    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data: User = await response.json();
        setUser(data);
      } catch (err) {
        setError(err instanceof Error ? err : new Error('Unknown error'));
      } finally {
        setLoading(false);
      }
    }

    fetchUser();

    // Cleanup function (optional)
    return () => {
      // Cancel requests, clear timers, etc.
    };
  }, [userId]);  // Dependency array

  // ...
}

useCallback and useMemo

import { useCallback, useMemo } from 'react';

type Item = {
  id: string;
  name: string;
  price: number;
};

function ShoppingCart({ items }: { items: Item[] }) {
  // useMemo: memoize computed values
  const total = useMemo((): number => {
    return items.reduce((sum, item) => sum + item.price, 0);
  }, [items]);

  // useCallback: memoize functions
  const handleRemove = useCallback((id: string): void => {
    console.log('Remove item:', id);
    // Remove logic...
  }, []);

  // With event parameter
  const handlePress = useCallback((item: Item): void => {
    console.log('Pressed:', item.name);
  }, []);

  return (
    <View>
      {items.map(item => (
        <Pressable key={item.id} onPress={() => handlePress(item)}>
          <Text>{item.name}</Text>
        </Pressable>
      ))}
      <Text>Total: ${total}</Text>
    </View>
  );
}

useReducer

import { useReducer } from 'react';

// State type
type CounterState = {
  count: number;
  step: number;
};

// Action types (discriminated union)
type CounterAction =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' }
  | { type: 'setStep'; payload: number };

// Reducer function
function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'reset':
      return { ...state, count: 0 };
    case 'setStep':
      return { ...state, step: action.payload };
    default:
      return state;
  }
}

// Usage in component
function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0, step: 1 });

  return (
    <View>
      <Text>Count: {state.count}</Text>
      <Button title="+" onPress={() => dispatch({ type: 'increment' })} />
      <Button title="-" onPress={() => dispatch({ type: 'decrement' })} />
      <Button title="Reset" onPress={() => dispatch({ type: 'reset' })} />
    </View>
  );
}

Typing Events

React Native events have their own types. Here are the most common ones you'll encounter:

Common Event Types

import { 
  GestureResponderEvent,
  NativeSyntheticEvent,
  TextInputChangeEventData,
  TextInputSubmitEditingEventData,
  LayoutChangeEvent,
  NativeScrollEvent
} from 'react-native';

// Pressable / TouchableOpacity events
function handlePress(event: GestureResponderEvent) {
  console.log('Pressed at:', event.nativeEvent.locationX);
}

// TextInput change event
function handleChange(event: NativeSyntheticEvent<TextInputChangeEventData>) {
  console.log('Text:', event.nativeEvent.text);
}

// TextInput submit event
function handleSubmit(event: NativeSyntheticEvent<TextInputSubmitEditingEventData>) {
  console.log('Submitted:', event.nativeEvent.text);
}

// Layout event
function handleLayout(event: LayoutChangeEvent) {
  const { width, height, x, y } = event.nativeEvent.layout;
  console.log('Layout:', { width, height, x, y });
}

// Scroll event
function handleScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {
  const { contentOffset, contentSize } = event.nativeEvent;
  console.log('Scroll position:', contentOffset.y);
}

Practical Examples

import { 
  TextInput, 
  Pressable, 
  ScrollView, 
  View,
  Text,
  GestureResponderEvent,
  NativeSyntheticEvent,
  NativeScrollEvent,
  LayoutChangeEvent
} from 'react-native';

function EventExamples() {
  // Most common: onPress just needs the handler
  const handlePress = (event: GestureResponderEvent) => {
    // Access native event details if needed
    console.log('Press location:', event.nativeEvent.pageX);
  };

  // TextInput: onChangeText gives you the string directly!
  const handleChangeText = (text: string) => {
    console.log('Text changed:', text);
  };

  // Scroll handling
  const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
    const offsetY = event.nativeEvent.contentOffset.y;
    console.log('Scrolled to:', offsetY);
  };

  // Layout measurement
  const handleLayout = (event: LayoutChangeEvent) => {
    const { width, height } = event.nativeEvent.layout;
    console.log('Component size:', width, height);
  };

  return (
    <View onLayout={handleLayout}>
      <Pressable onPress={handlePress}>
        <Text>Press me</Text>
      </Pressable>
      
      <TextInput onChangeText={handleChangeText} />
      
      <ScrollView onScroll={handleScroll}>
        {/* Content */}
      </ScrollView>
    </View>
  );
}

✅ Good News

For the most common cases, you don't need to type events manually:

  • onPress={() => doSomething()} — No typing needed
  • onChangeText={(text) => setText(text)} — text is inferred as string

You only need explicit event types when you need access to the full event object.

React Native Built-in Types

React Native exports many useful types. Here are the ones you'll use most often:

Style Types

import { 
  StyleSheet,
  ViewStyle, 
  TextStyle, 
  ImageStyle,
  StyleProp
} from 'react-native';

// Individual style types
const containerStyle: ViewStyle = {
  flex: 1,
  backgroundColor: '#fff',
  padding: 16,
};

const titleStyle: TextStyle = {
  fontSize: 24,
  fontWeight: 'bold',
  color: '#333',
};

const imageStyle: ImageStyle = {
  width: 100,
  height: 100,
  borderRadius: 50,
};

// For component props that accept styles
type CardProps = {
  style?: StyleProp<ViewStyle>;     // Accepts style prop
  titleStyle?: StyleProp<TextStyle>;
};

function Card({ style, titleStyle }: CardProps) {
  return (
    <View style={[styles.card, style]}>
      <Text style={[styles.title, titleStyle]}>Card</Text>
    </View>
  );
}

// StyleSheet.create returns typed styles
const styles = StyleSheet.create({
  card: {
    padding: 16,
    borderRadius: 8,
    backgroundColor: '#fff',
  },
  title: {
    fontSize: 18,
    fontWeight: '600',
  },
});

Component Props Types

import {
  ViewProps,
  TextProps,
  ImageProps,
  PressableProps,
  TextInputProps,
  ScrollViewProps,
  FlatListProps,
} from 'react-native';

// Extend existing component props
type CustomViewProps = ViewProps & {
  variant?: 'card' | 'container';
};

// FlatList needs a type parameter for the data
type Todo = { id: string; title: string };

const todoListProps: FlatListProps<Todo> = {
  data: [],
  renderItem: ({ item }) => <Text>{item.title}</Text>,
  keyExtractor: (item) => item.id,
};

Dimension and Layout Types

import { 
  Dimensions,
  ScaledSize,
  LayoutRectangle,
  LayoutChangeEvent
} from 'react-native';

// Get screen dimensions
const { width, height }: ScaledSize = Dimensions.get('window');

// Layout from onLayout event
function handleLayout(event: LayoutChangeEvent) {
  const layout: LayoutRectangle = event.nativeEvent.layout;
  console.log(layout.width, layout.height, layout.x, layout.y);
}

// Listen to dimension changes
import { useWindowDimensions } from 'react-native';

function ResponsiveComponent() {
  const { width, height } = useWindowDimensions();
  
  const isLandscape = width > height;
  
  return (
    <View style={{ flexDirection: isLandscape ? 'row' : 'column' }}>
      {/* Content */}
    </View>
  );
}

Platform Types

import { Platform } from 'react-native';

// Platform.OS is typed as 'ios' | 'android' | 'windows' | 'macos' | 'web'
const isIOS = Platform.OS === 'ios';
const isAndroid = Platform.OS === 'android';

// Platform.select is generic
const styles = StyleSheet.create({
  container: {
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 4,
      },
      android: {
        elevation: 4,
      },
      default: {},
    }),
  },
});

Common Patterns

Let's look at TypeScript patterns you'll use frequently in React Native development.

API Response Types

// Define your API response types
type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
};

type User = {
  id: string;
  name: string;
  email: string;
  avatar?: string;
};

type Post = {
  id: string;
  title: string;
  content: string;
  authorId: string;
  createdAt: string;
};

// Fetch with types
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const json: ApiResponse<User> = await response.json();
  return json.data;
}

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch('/api/posts');
  const json: ApiResponse<Post[]> = await response.json();
  return json.data;
}

Navigation Types (Expo Router)

// With Expo Router's typed routes
import { Link, router } from 'expo-router';

// Navigation is type-safe when typedRoutes is enabled in app.json
<Link href="/profile">Go to Profile</Link>
<Link href="/user/123">View User</Link>

// Programmatic navigation
router.push('/settings');
router.replace('/login');

// With parameters
router.push({
  pathname: '/user/[id]',
  params: { id: '123' }
});

Discriminated Unions for State

// Instead of multiple boolean flags, use discriminated unions
type LoadingState = { status: 'loading' };
type SuccessState<T> = { status: 'success'; data: T };
type ErrorState = { status: 'error'; error: Error };

type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;

function UserProfile() {
  const [state, setState] = useState<AsyncState<User>>({ 
    status: 'loading' 
  });

  // TypeScript knows the shape based on status
  if (state.status === 'loading') {
    return <ActivityIndicator />;
  }

  if (state.status === 'error') {
    return <Text>Error: {state.error.message}</Text>;
  }

  // TypeScript knows state.data exists here
  return <Text>{state.data.name}</Text>;
}

Generic Components

import { FlatList, Text, Pressable } from 'react-native';

// Generic list component
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
  onItemPress?: (item: T) => void;
};

function GenericList<T>({ 
  items, 
  renderItem, 
  keyExtractor,
  onItemPress 
}: ListProps<T>) {
  return (
    <FlatList
      data={items}
      keyExtractor={keyExtractor}
      renderItem={({ item }) => (
        <Pressable onPress={() => onItemPress?.(item)}>
          {renderItem(item)}
        </Pressable>
      )}
    />
  );
}

// Usage - TypeScript infers T from items
<GenericList
  items={users}
  renderItem={(user) => <Text>{user.name}</Text>}
  keyExtractor={(user) => user.id}
  onItemPress={(user) => console.log(user.email)}
/>

Type Assertions (When Needed)

// Sometimes you know more than TypeScript
const data = JSON.parse(jsonString) as User;

// For event targets (rare in RN, common in web)
const target = event.target as TextInput;

// Non-null assertion (use sparingly!)
const value = maybeNull!;  // Tells TS "I know this isn't null"

// Better: use optional chaining and nullish coalescing
const value = maybeNull ?? defaultValue;
const result = obj?.property?.nested;

⚠️ Avoid Overusing Type Assertions

Type assertions (as) and non-null assertions (!) bypass TypeScript's safety checks. Prefer proper typing, optional chaining (?.), and nullish coalescing (??) instead.

Summary

🎉 Key Takeaways

  • Props: Use type to define prop shapes, use ? for optional props
  • Hooks: Provide type parameters when initial value doesn't tell the full story
  • Events: Most events are auto-typed; explicit types only when accessing the full event
  • Styles: Use ViewStyle, TextStyle, ImageStyle and StyleProp
  • Extending: Combine native props with your own using & intersection
  • State: Use discriminated unions for complex state instead of multiple booleans

Quick Reference

// Props
type Props = { name: string; age?: number };

// State
const [user, setUser] = useState<User | null>(null);

// Ref
const inputRef = useRef<TextInput>(null);

// Style props
type CardProps = { style?: StyleProp<ViewStyle> };

// Extend native props
type ButtonProps = PressableProps & { title: string };

// Event handler
const handlePress = (event: GestureResponderEvent) => {};

// API types
type Response<T> = { data: T; status: number };

🎉 Module 2 Complete!

You've finished the Development Environment module! You now have:

  • ✅ Your tools installed and configured
  • ✅ A project created and running
  • ✅ An understanding of the development workflow
  • ✅ TypeScript knowledge for React Native

In Module 3, we dive into the Core Components — the building blocks of every React Native UI.

🚀 Ready to Build!

Your environment is set up, your TypeScript is sharp, and you know the workflow. Time to learn the components that make up every React Native app!