Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

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

TypeScript in React Native

Module Overview

Estimated Time: 3 hours | Difficulty: Intermediate | Prerequisites: Basic TypeScript knowledge
TypeScript is essential for building maintainable React Native applications. Without TypeScript, you are navigating without a map: prop mismatches, undefined navigation params, and mistyped style properties only show up at runtime — often on a device you do not have at your desk. TypeScript catches these at compile time, which is especially valuable in mobile development where the feedback loop (build, deploy to simulator, navigate to the right screen) is slower than web. This module covers TypeScript patterns specific to React Native, from basic component typing to advanced patterns used in production apps. What You’ll Learn:
  • TypeScript configuration for React Native
  • Typing components, props, and state
  • Navigation type safety
  • API response typing
  • Generic components
  • Advanced TypeScript patterns

TypeScript Setup

New Project with TypeScript

# Expo (TypeScript by default)
npx create-expo-app@latest my-app

# React Native CLI with TypeScript
npx react-native@latest init MyApp --template react-native-template-typescript

Adding TypeScript to Existing Project

# Install TypeScript and types
npm install -D typescript @types/react @types/react-native

# Create tsconfig.json
npx tsc --init

TypeScript Configuration

// tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    // Strict mode (recommended)
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    
    // Module resolution
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    
    // JSX
    "jsx": "react-native",
    
    // Output
    "noEmit": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    
    // Path aliases
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

Typing Components

Function Components

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

// Basic component with no props
function WelcomeMessage(): JSX.Element {
  return (
    <View style={styles.container}>
      <Text>Welcome!</Text>
    </View>
  );
}

// Component with props
interface GreetingProps {
  name: string;
  age?: number; // Optional prop
}

function Greeting({ name, age }: GreetingProps): JSX.Element {
  return (
    <View>
      <Text>Hello, {name}!</Text>
      {age && <Text>You are {age} years old</Text>}
    </View>
  );
}

// Using React.FC (less common now, but still valid)
const GreetingFC: React.FC<GreetingProps> = ({ name, age }) => {
  return (
    <View>
      <Text>Hello, {name}!</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 16,
  },
});

Props with Children

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

// Method 1: Explicit children type
interface CardProps {
  children: ReactNode;
  title?: string;
}

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

// Method 2: PropsWithChildren utility
import { PropsWithChildren } from 'react';

interface CardBaseProps {
  title?: string;
  variant?: 'default' | 'outlined';
}

type CardProps = PropsWithChildren<CardBaseProps>;

function Card({ children, title, variant = 'default' }: CardProps) {
  return (
    <View style={[styles.card, variant === 'outlined' && styles.outlined]}>
      {title && <Text>{title}</Text>}
      {children}
    </View>
  );
}

// Method 3: Extending native component props
interface ContainerProps extends ViewProps {
  centered?: boolean;
}

function Container({ centered, style, children, ...props }: ContainerProps) {
  return (
    <View
      style={[styles.container, centered && styles.centered, style]}
      {...props}
    >
      {children}
    </View>
  );
}

Event Handler Props

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

interface ButtonProps {
  title: string;
  onPress: (event: GestureResponderEvent) => void;
  onLongPress?: (event: GestureResponderEvent) => void;
  disabled?: boolean;
}

function Button({ title, onPress, onLongPress, disabled }: ButtonProps) {
  return (
    <Pressable
      onPress={onPress}
      onLongPress={onLongPress}
      disabled={disabled}
      style={({ pressed }) => [
        styles.button,
        pressed && styles.pressed,
        disabled && styles.disabled,
      ]}
    >
      <Text style={styles.text}>{title}</Text>
    </Pressable>
  );
}

// Usage
<Button
  title="Submit"
  onPress={(e) => console.log('Pressed', e.nativeEvent)}
  onLongPress={(e) => console.log('Long pressed')}
/>

Typing State

useState

import { useState } from 'react';

// Type inference (preferred when possible)
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [isLoading, setIsLoading] = useState(false); // boolean

// Explicit typing (when needed)
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<string[]>([]);
const [data, setData] = useState<ApiResponse | undefined>(undefined);

// Complex state
interface FormState {
  email: string;
  password: string;
  rememberMe: boolean;
}

const [form, setForm] = useState<FormState>({
  email: '',
  password: '',
  rememberMe: false,
});

// Update partial state
setForm(prev => ({ ...prev, email: 'new@email.com' }));

useReducer

import { useReducer } from 'react';

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

// Action types
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
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' })} />
      <Button
        title="Set Step to 5"
        onPress={() => dispatch({ type: 'setStep', payload: 5 })}
      />
    </View>
  );
}

Typing Refs

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

// Ref to native component
function SearchInput() {
  const inputRef = useRef<TextInput>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <View>
      <TextInput ref={inputRef} placeholder="Search..." />
      <Button title="Focus" onPress={focusInput} />
    </View>
  );
}

// Ref to Animated.Value
function FadeView() {
  const opacity = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }).start();
  };

  return (
    <Animated.View style={{ opacity }}>
      <Text>Fading content</Text>
    </Animated.View>
  );
}

// forwardRef with TypeScript
interface CustomInputProps {
  label: string;
  error?: string;
}

interface CustomInputRef {
  focus: () => void;
  blur: () => void;
  clear: () => void;
}

const CustomInput = forwardRef<CustomInputRef, CustomInputProps>(
  ({ label, error }, ref) => {
    const inputRef = useRef<TextInput>(null);

    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      blur: () => inputRef.current?.blur(),
      clear: () => inputRef.current?.clear(),
    }));

    return (
      <View>
        <Text>{label}</Text>
        <TextInput ref={inputRef} />
        {error && <Text style={styles.error}>{error}</Text>}
      </View>
    );
  }
);

// Usage
function Form() {
  const inputRef = useRef<CustomInputRef>(null);

  return (
    <View>
      <CustomInput ref={inputRef} label="Email" />
      <Button title="Focus" onPress={() => inputRef.current?.focus()} />
    </View>
  );
}

Typing Context

import { createContext, useContext, useState, ReactNode } from 'react';

// Define types
interface User {
  id: string;
  email: string;
  name: string;
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
}

// Create context with default value
const AuthContext = createContext<AuthContextType | undefined>(undefined);

// Provider component
interface AuthProviderProps {
  children: ReactNode;
}

export function AuthProvider({ children }: AuthProviderProps) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const login = async (email: string, password: string) => {
    setIsLoading(true);
    try {
      // API call
      const response = await authService.login(email, password);
      setUser(response.user);
    } finally {
      setIsLoading(false);
    }
  };

  const logout = async () => {
    await authService.logout();
    setUser(null);
  };

  const register = async (email: string, password: string, name: string) => {
    setIsLoading(true);
    try {
      const response = await authService.register(email, password, name);
      setUser(response.user);
    } finally {
      setIsLoading(false);
    }
  };

  const value: AuthContextType = {
    user,
    isAuthenticated: !!user,
    isLoading,
    login,
    logout,
    register,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Custom hook with type safety
export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

// Usage
function ProfileScreen() {
  const { user, logout, isLoading } = useAuth();

  if (!user) return null;

  return (
    <View>
      <Text>Welcome, {user.name}</Text>
      <Button title="Logout" onPress={logout} disabled={isLoading} />
    </View>
  );
}

Typing Navigation

React Navigation Types

Typing your navigation is one of the highest-value TypeScript investments in a React Native project. Without it, passing wrong params to a screen is a runtime crash that only surfaces when a tester taps the right button. With proper typing, the compiler catches it instantly. The setup below looks like a lot of boilerplate, but you write it once and it protects every navigation.navigate() call in your entire app.
// src/shared/types/navigation.types.ts
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps, NavigatorScreenParams } from '@react-navigation/native';

// Root Stack
export type RootStackParamList = {
  Auth: NavigatorScreenParams<AuthStackParamList>;
  Main: NavigatorScreenParams<MainTabParamList>;
  Modal: { title: string; content: string };
  ProductDetail: { productId: string };
};

// Auth Stack
export type AuthStackParamList = {
  Login: undefined;
  Register: undefined;
  ForgotPassword: { email?: string };
};

// Main Tabs
export type MainTabParamList = {
  Home: undefined;
  Search: { query?: string };
  Cart: undefined;
  Profile: undefined;
};

// Screen props types
export type RootStackScreenProps<T extends keyof RootStackParamList> =
  NativeStackScreenProps<RootStackParamList, T>;

export type AuthStackScreenProps<T extends keyof AuthStackParamList> =
  CompositeScreenProps<
    NativeStackScreenProps<AuthStackParamList, T>,
    RootStackScreenProps<keyof RootStackParamList>
  >;

export type MainTabScreenProps<T extends keyof MainTabParamList> =
  CompositeScreenProps<
    BottomTabScreenProps<MainTabParamList, T>,
    RootStackScreenProps<keyof RootStackParamList>
  >;

// Declare global types for useNavigation
declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

Using Navigation Types

// screens/LoginScreen.tsx
import { View, Text, Button } from 'react-native';
import type { AuthStackScreenProps } from '@/shared/types/navigation.types';

type Props = AuthStackScreenProps<'Login'>;

export function LoginScreen({ navigation, route }: Props) {
  const handleLogin = () => {
    // Navigate to main app
    navigation.replace('Main', { screen: 'Home' });
  };

  const handleForgotPassword = () => {
    // Navigate with params
    navigation.navigate('ForgotPassword', { email: 'user@example.com' });
  };

  return (
    <View>
      <Text>Login Screen</Text>
      <Button title="Login" onPress={handleLogin} />
      <Button title="Forgot Password" onPress={handleForgotPassword} />
    </View>
  );
}

// screens/ProductDetailScreen.tsx
import type { RootStackScreenProps } from '@/shared/types/navigation.types';

type Props = RootStackScreenProps<'ProductDetail'>;

export function ProductDetailScreen({ navigation, route }: Props) {
  const { productId } = route.params; // Type-safe params

  return (
    <View>
      <Text>Product ID: {productId}</Text>
      <Button title="Go Back" onPress={() => navigation.goBack()} />
    </View>
  );
}

useNavigation and useRoute Hooks

import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import type { RootStackParamList } from '@/shared/types/navigation.types';

// In a component
function ProductCard({ productId }: { productId: string }) {
  // Type the navigation hook
  const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();

  const handlePress = () => {
    navigation.navigate('ProductDetail', { productId });
  };

  return (
    <Pressable onPress={handlePress}>
      <Text>View Product</Text>
    </Pressable>
  );
}

// Getting route params
function ProductDetailScreen() {
  const route = useRoute<RouteProp<RootStackParamList, 'ProductDetail'>>();
  const { productId } = route.params;

  return <Text>Product: {productId}</Text>;
}

Typing API Responses

// src/shared/types/api.types.ts

// Generic API response wrapper
interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

interface ApiError {
  message: string;
  code: string;
  details?: Record<string, string[]>;
}

// Domain types
interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
  createdAt: string;
}

interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  images: string[];
  category: Category;
  inStock: boolean;
}

interface Category {
  id: string;
  name: string;
  slug: string;
}

// API service with types
const apiClient = {
  async get<T>(url: string): Promise<T> {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  },

  async post<T, D>(url: string, data: D): Promise<T> {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  },
};

// Usage
async function fetchProducts(): Promise<PaginatedResponse<Product>> {
  return apiClient.get<PaginatedResponse<Product>>('/api/products');
}

async function createUser(data: { email: string; password: string; name: string }): Promise<ApiResponse<User>> {
  return apiClient.post<ApiResponse<User>, typeof data>('/api/users', data);
}

Typing with React Query

import { useQuery, useMutation, UseQueryResult } from '@tanstack/react-query';

interface Product {
  id: string;
  name: string;
  price: number;
}

interface CreateProductInput {
  name: string;
  price: number;
  description: string;
}

// Typed query hook
function useProducts(): UseQueryResult<Product[], Error> {
  return useQuery({
    queryKey: ['products'],
    queryFn: async () => {
      const response = await fetch('/api/products');
      if (!response.ok) throw new Error('Failed to fetch');
      return response.json();
    },
  });
}

// Typed mutation hook
function useCreateProduct() {
  return useMutation<Product, Error, CreateProductInput>({
    mutationFn: async (input) => {
      const response = await fetch('/api/products', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
      });
      if (!response.ok) throw new Error('Failed to create');
      return response.json();
    },
  });
}

// Usage in component
function ProductList() {
  const { data: products, isLoading, error } = useProducts();
  const createProduct = useCreateProduct();

  if (isLoading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <View>
      {products?.map((product) => (
        <Text key={product.id}>{product.name}</Text>
      ))}
      <Button
        title="Add Product"
        onPress={() => createProduct.mutate({ name: 'New', price: 99, description: 'Desc' })}
      />
    </View>
  );
}

Generic Components

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

// Generic list component
interface GenericListProps<T> extends Omit<FlatListProps<T>, 'renderItem'> {
  data: T[];
  renderItem: (item: T, index: number) => JSX.Element;
  keyExtractor: (item: T) => string;
  emptyMessage?: string;
}

function GenericList<T>({
  data,
  renderItem,
  keyExtractor,
  emptyMessage = 'No items',
  ...props
}: GenericListProps<T>) {
  if (data.length === 0) {
    return (
      <View style={styles.empty}>
        <Text>{emptyMessage}</Text>
      </View>
    );
  }

  return (
    <FlatList
      data={data}
      renderItem={({ item, index }) => renderItem(item, index)}
      keyExtractor={keyExtractor}
      {...props}
    />
  );
}

// Usage
interface User {
  id: string;
  name: string;
}

function UserList() {
  const users: User[] = [
    { id: '1', name: 'John' },
    { id: '2', name: 'Jane' },
  ];

  return (
    <GenericList<User>
      data={users}
      renderItem={(user) => <Text>{user.name}</Text>}
      keyExtractor={(user) => user.id}
      emptyMessage="No users found"
    />
  );
}

Generic Select Component

interface SelectOption<T> {
  label: string;
  value: T;
}

interface SelectProps<T> {
  options: SelectOption<T>[];
  value: T | null;
  onChange: (value: T) => void;
  placeholder?: string;
}

function Select<T>({ options, value, onChange, placeholder }: SelectProps<T>) {
  const selectedOption = options.find((opt) => opt.value === value);

  return (
    <View style={styles.select}>
      <Text>{selectedOption?.label ?? placeholder ?? 'Select...'}</Text>
      {options.map((option, index) => (
        <Pressable
          key={index}
          onPress={() => onChange(option.value)}
          style={styles.option}
        >
          <Text>{option.label}</Text>
        </Pressable>
      ))}
    </View>
  );
}

// Usage with different types
type Status = 'pending' | 'active' | 'completed';

function StatusSelect() {
  const [status, setStatus] = useState<Status | null>(null);

  const options: SelectOption<Status>[] = [
    { label: 'Pending', value: 'pending' },
    { label: 'Active', value: 'active' },
    { label: 'Completed', value: 'completed' },
  ];

  return (
    <Select<Status>
      options={options}
      value={status}
      onChange={setStatus}
      placeholder="Select status"
    />
  );
}

Utility Types

// Common utility types for React Native

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties required
type RequiredUser = Required<User>;

// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;

// Omit specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Extract props from component
type ButtonProps = React.ComponentProps<typeof Button>;

// Extract style props
type ViewStyle = React.ComponentProps<typeof View>['style'];

// Readonly
type ReadonlyUser = Readonly<User>;

// Record type
type UserMap = Record<string, User>;

// Custom utility types
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
type AsyncFunction<T> = () => Promise<T>;

// Discriminated unions
type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function useAsyncData<T>(): LoadingState<T> {
  // Implementation
}

// Usage
function DataDisplay() {
  const state = useAsyncData<User[]>();

  switch (state.status) {
    case 'idle':
      return <Text>Ready to load</Text>;
    case 'loading':
      return <ActivityIndicator />;
    case 'success':
      return <UserList users={state.data} />;
    case 'error':
      return <Text>Error: {state.error.message}</Text>;
  }
}

Type Guards

// Type guards for runtime type checking

interface User {
  type: 'user';
  id: string;
  name: string;
}

interface Admin {
  type: 'admin';
  id: string;
  name: string;
  permissions: string[];
}

type Account = User | Admin;

// Type guard function
function isAdmin(account: Account): account is Admin {
  return account.type === 'admin';
}

// Usage
function AccountInfo({ account }: { account: Account }) {
  if (isAdmin(account)) {
    // TypeScript knows account is Admin here
    return (
      <View>
        <Text>{account.name} (Admin)</Text>
        <Text>Permissions: {account.permissions.join(', ')}</Text>
      </View>
    );
  }

  // TypeScript knows account is User here
  return (
    <View>
      <Text>{account.name}</Text>
    </View>
  );
}

// Type guard for API responses
interface SuccessResponse<T> {
  success: true;
  data: T;
}

interface ErrorResponse {
  success: false;
  error: string;
}

type ApiResult<T> = SuccessResponse<T> | ErrorResponse;

function isSuccess<T>(result: ApiResult<T>): result is SuccessResponse<T> {
  return result.success === true;
}

// Usage
async function fetchUser(id: string) {
  const result = await api.get<ApiResult<User>>(`/users/${id}`);

  if (isSuccess(result)) {
    return result.data; // Type: User
  } else {
    throw new Error(result.error);
  }
}

Best Practices

Enable Strict Mode

Always use "strict": true in tsconfig.json for maximum type safety

Avoid 'any'

Use unknown instead of any when type is truly unknown

Type Inference

Let TypeScript infer types when possible; explicit types when needed

Discriminated Unions

Use discriminated unions for state that can be in different shapes

Common Mistakes to Avoid

// ❌ Bad: Using 'any'
const handleData = (data: any) => { };

// ✅ Good: Use proper types or 'unknown'
const handleData = (data: unknown) => {
  if (typeof data === 'string') {
    // Now TypeScript knows data is string
  }
};

// ❌ Bad: Non-null assertion without checking
const user = users.find(u => u.id === id)!;

// ✅ Good: Handle the undefined case
const user = users.find(u => u.id === id);
if (!user) {
  throw new Error('User not found');
}

// ❌ Bad: Type assertion without validation
const data = response as User;

// ✅ Good: Validate the data
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data
  );
}

if (isUser(response)) {
  // Now TypeScript knows response is User
}

Platform-Specific Typing

React Native components often have props that only work on one platform. TypeScript helps you catch these mismatches, but only if you understand the type definitions.
import { TextInput, Platform, TextInputProps } from 'react-native';

// Some TextInput props are iOS-only or Android-only.
// TypeScript allows all props on both platforms because the types are
// a union. The compiler will NOT warn you that textAlignVertical is
// Android-only or that clearButtonMode is iOS-only.
// You must rely on documentation and testing.

interface SearchInputProps {
  value: string;
  onChangeText: (text: string) => void;
}

function SearchInput({ value, onChangeText }: SearchInputProps) {
  return (
    <TextInput
      value={value}
      onChangeText={onChangeText}
      placeholder="Search..."
      // clearButtonMode is iOS-only -- renders a small "X" button inside the input.
      // On Android, this prop is silently ignored. If you need a clear button on
      // Android, you must build it yourself with a Pressable overlay.
      clearButtonMode="while-editing"
      // textAlignVertical is Android-only -- controls vertical text alignment in
      // multiline inputs. iOS always aligns multiline text to the top by default.
      textAlignVertical="top"
      // Platform-specific keyboard appearance
      keyboardAppearance={Platform.OS === 'ios' ? 'light' : undefined}
    />
  );
}

// Typing platform-specific style values
import { ViewStyle } from 'react-native';

// iOS shadow props exist in ViewStyle but have no effect on Android.
// Android's 'elevation' exists in ViewStyle but has no effect on iOS.
// TypeScript accepts both on either platform -- runtime behavior differs.
function createShadowStyle(): ViewStyle {
  return Platform.select({
    ios: {
      shadowColor: '#000',
      shadowOffset: { width: 0, height: 2 },
      shadowOpacity: 0.25,
      shadowRadius: 4,
    },
    android: {
      elevation: 4,
    },
    default: {},
  }) as ViewStyle;
}
Key insight for mobile TypeScript: React Native’s type definitions are a lowest-common-denominator union of iOS and Android capabilities. TypeScript will happily let you set android_ripple on iOS (where it is silently ignored) or clearButtonMode on Android (where it does nothing). Type safety tells you the prop exists on the component — it does not tell you whether it works on the current platform. Always cross-reference the React Native docs for platform-specific behavior, and test on both iOS and Android simulators.

Mobile TypeScript Pitfalls

TypeScript pitfalls specific to React Native:Native module types lag behind. Third-party native modules (camera, maps, Bluetooth) often have incomplete or outdated type definitions. When @types/some-library does not exist, you face a choice: write declare module 'some-library' with minimal types, or use // @ts-ignore. Prefer the former — even a partial declaration with any return types is better than no types, because it at least documents which functions exist.Navigation params are not runtime-validated. TypeScript ensures you pass the right params at compile time, but deep links and push notification handlers bypass your navigation calls entirely. A deep link to /product/abc will populate route.params.id as a string even if your type says id: number. Always validate params from external sources at runtime, even when TypeScript says the types match.Platform.select() return type is too broad. TypeScript infers the return type as the union of all branches, which means the iOS-specific branch’s types leak into the Android code path and vice versa. If this causes issues, use explicit type annotations or as assertions on the result.Hermes does not support all ES6+ features. While TypeScript compiles to JS, the runtime engine (Hermes) has a limited feature set. Features like Proxy, Reflect.construct, and some Intl APIs are missing or incomplete. TypeScript will happily let you write code that uses these — the error only appears at runtime on the device. Check the Hermes language features table when using advanced JS features.

Next Steps

Module 5: Core Components Deep Dive

Master React Native’s fundamental building blocks with TypeScript