Why TypeScript?
- Type safety: Catch errors before runtime
- Better IDE support: Autocomplete, refactoring, navigation
- Self-documenting: Types serve as inline documentation
- Easier refactoring: Rename and restructure with confidence
- Better tooling: Enhanced debugging and testing
Setting Up TypeScript
New Projects
React Native CLI creates TypeScript projects by default:npx react-native@latest init MyApp
Existing JavaScript Projects
Add TypeScript to an existing project:npm install --save-dev typescript @types/react @types/react-native
tsconfig.json:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": ["es2019"],
"allowJs": true,
"jsx": "react-native",
"noEmit": true,
"isolatedModules": true,
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"babel.config.js",
"metro.config.js",
"jest.config.js"
]
}
# Rename .js to .tsx for components
mv App.js App.tsx
# Rename .js to .ts for utilities
mv utils.js utils.ts
Type Definitions
React Native Types
React Native includes built-in TypeScript definitions:import {View, Text, StyleSheet, ViewStyle, TextStyle} from 'react-native';
interface Styles {
container: ViewStyle;
title: TextStyle;
}
const styles = StyleSheet.create<Styles>({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
Component Props
import React from 'react';
import {View, Text, TouchableOpacity} from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
}
function Button({
title,
onPress,
disabled = false,
variant = 'primary',
}: ButtonProps) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled}
style={[styles.button, styles[variant]]}
>
<Text style={styles.text}>{title}</Text>
</TouchableOpacity>
);
}
export default Button;
Children Props
import React, {ReactNode} from 'react';
import {View} from 'react-native';
interface CardProps {
children: ReactNode;
padding?: number;
}
function Card({children, padding = 16}: CardProps) {
return (
<View style={{padding}}>
{children}
</View>
);
}
Hooks with TypeScript
useState
import {useState} from 'react';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
// Type inference
const [count, setCount] = useState(0);
// Explicit type
const [user, setUser] = useState<User | null>(null);
// Array type
const [items, setItems] = useState<string[]>([]);
return (
<View>
<Text>Count: {count}</Text>
{user && <Text>{user.name}</Text>}
</View>
);
}
useEffect
import {useEffect} from 'react';
function Timer() {
const [seconds, setSeconds] = useState<number>(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function is typed automatically
return () => clearInterval(interval);
}, []); // Dependencies array is type-checked
return <Text>{seconds}s</Text>;
}
useRef
import {useRef} from 'react';
import {TextInput} from 'react-native';
function Form() {
// Ref for React Native component
const inputRef = useRef<TextInput>(null);
// Ref for mutable value
const countRef = useRef<number>(0);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<TextInput
ref={inputRef}
placeholder="Enter text"
/>
);
}
Custom Hooks
import {useState, useEffect} from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setState({data, loading: false, error: null}))
.catch(error => setState({data: null, loading: false, error}));
}, [url]);
return state;
}
// Usage
interface User {
id: number;
name: string;
}
function UserList() {
const {data, loading, error} = useFetch<User[]>('/api/users');
if (loading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return (
<View>
{data?.map(user => (
<Text key={user.id}>{user.name}</Text>
))}
</View>
);
}
Navigation Types
React Navigation
import {NavigatorScreenParams} from '@react-navigation/native';
import {NativeStackScreenProps} from '@react-navigation/native-stack';
// Define navigation structure
type RootStackParamList = {
Home: undefined;
Profile: {userId: string};
Settings: {theme?: 'light' | 'dark'};
};
// Screen component props
type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
function HomeScreen({navigation, route}: HomeScreenProps) {
const navigateToProfile = () => {
navigation.navigate('Profile', {userId: '123'});
};
return (
<View>
<Button title="View Profile" onPress={navigateToProfile} />
</View>
);
}
// Type-safe navigation hook
import {useNavigation} from '@react-navigation/native';
import {NativeStackNavigationProp} from '@react-navigation/native-stack';
type HomeScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'Home'
>;
function useHomeNavigation() {
return useNavigation<HomeScreenNavigationProp>();
}
Style Types
Typed StyleSheet
import {StyleSheet, ViewStyle, TextStyle, ImageStyle} from 'react-native';
type Styles = {
container: ViewStyle;
title: TextStyle;
image: ImageStyle;
};
const styles = StyleSheet.create<Styles>({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
},
image: {
width: 100,
height: 100,
resizeMode: 'cover',
},
});
Dynamic Styles
import {ViewStyle} from 'react-native';
interface ThemeColors {
primary: string;
secondary: string;
background: string;
}
function createStyles(theme: ThemeColors) {
return StyleSheet.create({
container: {
backgroundColor: theme.background,
} as ViewStyle,
button: {
backgroundColor: theme.primary,
} as ViewStyle,
});
}
API Types
Fetch with Types
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const json: ApiResponse<User> = await response.json();
return json.data;
}
// Usage
useEffect(() => {
fetchUser(123).then(user => {
console.log(user.name); // Type-safe access
});
}, []);
AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';
interface StorageKeys {
USER_TOKEN: string;
USER_PREFERENCES: UserPreferences;
THEME: 'light' | 'dark';
}
interface UserPreferences {
notifications: boolean;
language: string;
}
class TypedStorage {
static async getItem<K extends keyof StorageKeys>(
key: K,
): Promise<StorageKeys[K] | null> {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
static async setItem<K extends keyof StorageKeys>(
key: K,
value: StorageKeys[K],
): Promise<void> {
await AsyncStorage.setItem(key, JSON.stringify(value));
}
}
// Usage - fully typed!
const prefs = await TypedStorage.getItem('USER_PREFERENCES');
if (prefs) {
console.log(prefs.notifications); // Type-safe
}
Event Handlers
Touch Events
import {GestureResponderEvent} from 'react-native';
interface Props {
onPress: (event: GestureResponderEvent) => void;
}
function CustomButton({onPress}: Props) {
const handlePress = (event: GestureResponderEvent) => {
console.log('Pressed at', event.nativeEvent.pageX, event.nativeEvent.pageY);
onPress(event);
};
return (
<TouchableOpacity onPress={handlePress}>
<Text>Press Me</Text>
</TouchableOpacity>
);
}
Text Input Events
import {NativeSyntheticEvent, TextInputChangeEventData} from 'react-native';
function SearchInput() {
const [query, setQuery] = useState<string>('');
const handleChange = (
event: NativeSyntheticEvent<TextInputChangeEventData>,
) => {
const text = event.nativeEvent.text;
setQuery(text);
};
return (
<TextInput
value={query}
onChange={handleChange}
placeholder="Search..."
/>
);
}
Context with TypeScript
import React, {createContext, useContext, useState, ReactNode} from 'react';
interface User {
id: number;
name: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({children}: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const login = (user: User) => setUser(user);
const logout = () => setUser(null);
const isAuthenticated = user !== null;
return (
<AuthContext.Provider value={{user, login, logout, isAuthenticated}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
// Usage
function ProfileScreen() {
const {user, isAuthenticated, logout} = useAuth();
if (!isAuthenticated) {
return <Text>Please log in</Text>;
}
return (
<View>
<Text>Welcome, {user.name}!</Text>
<Button title="Logout" onPress={logout} />
</View>
);
}
Advanced TypeScript Patterns
Generic Components
interface ListProps<T> {
data: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({data, renderItem, keyExtractor}: ListProps<T>) {
return (
<View>
{data.map(item => (
<View key={keyExtractor(item)}>
{renderItem(item)}
</View>
))}
</View>
);
}
// Usage
interface User {
id: number;
name: string;
}
function UserList() {
const users: User[] = [{id: 1, name: 'John'}];
return (
<List
data={users}
renderItem={user => <Text>{user.name}</Text>}
keyExtractor={user => user.id.toString()}
/>
);
}
Utility Types
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Omit sensitive fields
type PublicUser = Omit<User, 'password'>;
// Pick specific fields
type UserPreview = Pick<User, 'id' | 'name'>;
// Make all fields optional
type PartialUser = Partial<User>;
// Make all fields required
type RequiredUser = Required<Partial<User>>;
// Make all fields readonly
type ImmutableUser = Readonly<User>;
Testing with TypeScript
import {render, fireEvent} from '@testing-library/react-native';
import {Button} from './Button';
describe('Button', () => {
it('calls onPress when pressed', () => {
const onPress = jest.fn();
const {getByText} = render(
<Button title="Click" onPress={onPress} />
);
fireEvent.press(getByText('Click'));
expect(onPress).toHaveBeenCalledTimes(1);
});
});
Configuration Tips
Strict Mode
Enable strict type checking:{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Path Mapping
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"],
"@hooks/*": ["hooks/*"]
}
}
}
import {Button} from '@components/Button';
import {useAuth} from '@hooks/useAuth';
Common Issues
Type errors with third-party libraries
Type errors with third-party libraries
Install type definitions:Or create custom type declarations in
npm install --save-dev @types/library-name
types.d.ts:declare module 'library-name' {
export function someFunction(): void;
}
Cannot find module errors
Cannot find module errors
Ensure
moduleResolution is set correctly:{
"compilerOptions": {
"moduleResolution": "node"
}
}
Best Practices
- Enable strict mode: Catch more errors at compile time
- Avoid
anytype: Useunknownor proper types instead - Use interfaces for props: More readable and maintainable
- Type custom hooks: Ensure type safety throughout
- Create type utilities: Reuse common type patterns
- Document with JSDoc: Add comments for complex types
- Use type guards: Safely narrow types at runtime
Next Steps
Testing
Test TypeScript React Native apps
Security
Security best practices