Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/facebook/react-native/llms.txt

Use this file to discover all available pages before exploring further.

TypeScript adds static typing to JavaScript, helping catch errors at compile time and improving code quality. React Native has first-class TypeScript support.

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
Create 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 files:
# 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>
  );
}

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/*"]
    }
  }
}
Usage:
import {Button} from '@components/Button';
import {useAuth} from '@hooks/useAuth';

Common Issues

Install type definitions:
npm install --save-dev @types/library-name
Or create custom type declarations in types.d.ts:
declare module 'library-name' {
  export function someFunction(): void;
}
Ensure moduleResolution is set correctly:
{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

Best Practices

  1. Enable strict mode: Catch more errors at compile time
  2. Avoid any type: Use unknown or proper types instead
  3. Use interfaces for props: More readable and maintainable
  4. Type custom hooks: Ensure type safety throughout
  5. Create type utilities: Reuse common type patterns
  6. Document with JSDoc: Add comments for complex types
  7. Use type guards: Safely narrow types at runtime

Next Steps

Testing

Test TypeScript React Native apps

Security

Security best practices

Build docs developers (and LLMs) love