🔷 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:
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 neededonChangeText={(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
typeto 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,ImageStyleandStyleProp - 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!