Skip to main content
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