TypeScript in React Native

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Every code example so far has already been TypeScript, because that is what create-expo-app generates by default. This page covers what is specific to typing a React Native project rather than a web one: the project’s tsconfig.json, the Strict TypeScript API that governs which import paths are allowed to exist, and typing the Core Components' props, refs, and events. It does not re-cover the TypeScript language itself or React’s own typing patterns — the TypeScript reference and TypeScript with React already do, in full.

TypeScript as the Template Default

npx create-expo-app@latest (used since Getting Started) scaffolds a project whose tsconfig.json extends Expo’s own base configuration, and the equivalent Community CLI template extends @react-native/typescript-config instead — both resolve to the same effective strict settings, so Places is written against either without any per-page distinction:

// tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}

(The Community CLI equivalent is identical apart from "extends": "@react-native/typescript-config".) tsconfig and the Compiler Options covers what each option in the base configuration actually does; nothing about strict, module resolution, or paths is React-Native-specific.

The Strict TypeScript API

react-native’s own package used to export everything from one flat namespace, including many internal modules never meant for application code to import directly. The Strict TypeScript API closes that surface down to root exports only — everything a project needs is importable from ’react-native' itself, and the package’s internal file layout is no longer part of its public contract:

// Accepted: importing from the package root.
import { View, Text, Image, Pressable } from 'react-native';
import type { ViewProps, ViewInstance } from 'react-native';

// Rejected under the Strict TypeScript API: reaching into an internal path.
import ViewNativeComponent from 'react-native/Libraries/Components/View/ViewNativeComponent';

Two consequences follow directly from that root-exports-only surface:

  • Types are generated from source, not hand-maintained separately — ViewProps, TextProps, and every other exported type ships from the same source files as the runtime code, so a type can never drift out of sync with the implementation it describes the way a hand-written .d.ts could.

  • Ref types are named, exported types — ViewInstance and TextInputInstance (used below) are the typed handles a ref on View/TextInput actually resolves to, replacing the looser, implementation-shaped ref types earlier React Native versions exposed.

A customConditions package-resolution opt-out remains available through React Native 0.87 for a codebase that still imports deep, unmigrated paths from third-party packages built against the old surface; it is scheduled for removal in React Native 0.88, so Places — created fresh on 0.87 — never enables it, and any project still relying on it should treat 0.87 as the deadline to finish migrating those imports to their package’s public root export before upgrading further.

flowchart LR A["import { View } from 'react-native'"] -->|root export| B[Resolved: Strict TypeScript API] C["import ViewNativeComponent from\n'react-native/Libraries/.../ViewNativeComponent'"] -->|deep import| D[Rejected: not part of the public API] E["customConditions opt-out (removed in 0.88)"] -.->|temporary escape hatch| C

Typing Props, Refs, and Events

Every Core Component ships its own props type, composable the same way a web component’s prop types are:

import type { ViewProps, TextProps, PressableProps, StyleProp, ViewStyle } from 'react-native';

interface CardContainerProps extends ViewProps {
  style?: StyleProp<ViewStyle>;
}

interface CardLabelProps extends TextProps {
  emphasis?: boolean;
}

interface CardPressableProps extends Omit<PressableProps, 'onPress'> {
  onPress: (id: string) => void;
}

StyleProp<ViewStyle> (rather than ViewStyle alone) is what every style-accepting prop should be typed as, since it also accepts the arrays and falsy-conditional style values Styling (later in this section) covers.

A typed ref uses the Strict TypeScript API’s own instance types, not a bare any or React.Component:

import { useRef } from 'react';
import type { TextInputInstance } from 'react-native';

function useFocusOnMount() {
  const inputRef = useRef<TextInputInstance>(null);
  return inputRef; // inputRef.current?.focus() is fully typed
}

Native events carry their own generic wrapper and payload types instead of the DOM’s Event/ChangeEvent:

import type {
  NativeSyntheticEvent,
  TextInputChangeEventData,
  LayoutChangeEvent,
  GestureResponderEvent,
} from 'react-native';

function onNameChange(event: NativeSyntheticEvent<TextInputChangeEventData>) {
  console.log(event.nativeEvent.text);
}

function onCardLayout(event: LayoutChangeEvent) {
  const { width, height } = event.nativeEvent.layout;
  console.log(width, height);
}

function onCardPressIn(event: GestureResponderEvent) {
  console.log(event.nativeEvent.pageX, event.nativeEvent.pageY);
}

Two more typed surfaces reuse this same shape further into this section rather than introducing a new pattern: typed navigation params (RootStackParamList/TabParamList, covered on Navigation) and typed Turbo Native Module specs (the Spec interface pattern already shown on Architecture Under the Hood, covered in full on Native Modules and Components).

For generic prop/state typing patterns themselves — typing children, discriminated union props, generic components — see TypeScript with React; for the type system underneath all of it, see the TypeScript reference.

PlaceCard, Fully Typed

Extending the untyped PlaceCard with an accepted StyleProp<ViewStyle> override and a ViewInstance ref — the shape every later Places component that needs to expose its outer container follows. React 19 passes ref to a function component as an ordinary prop, so it is declared and typed alongside the others rather than wrapped in forwardRef:

// src/features/places/components/PlaceCard.tsx
import type { Ref } from 'react';
import { Image, Pressable, StyleSheet, Text, View } from 'react-native';
import type { StyleProp, ViewStyle, ViewInstance } from 'react-native';
import type { Place } from '../types';

interface PlaceCardProps {
  place: Place;
  style?: StyleProp<ViewStyle>;
  ref?: Ref<ViewInstance>;
  onPress: (placeId: string) => void;
  onToggleFavourite: (placeId: string) => void;
}

export function PlaceCard({ place, style, ref, onPress, onToggleFavourite }: PlaceCardProps) {
  return (
    <View ref={ref} style={[styles.card, style]}>
      <Pressable style={styles.pressable} onPress={() => onPress(place.id)}>
        {place.photoUri !== null && (
          <Image source={{ uri: place.photoUri }} style={styles.photo} resizeMode="cover" />
        )}
        <View style={styles.body}>
          <Text style={styles.name}>{place.name}</Text>
          <Text style={styles.category}>{place.category}</Text>
        </View>
      </Pressable>
      <Pressable
        onPress={() => onToggleFavourite(place.id)}
        hitSlop={8}
        accessibilityLabel={place.favourite ? 'Remove from favourites' : 'Add to favourites'}
      >
        <Text style={styles.favourite}>{place.favourite ? '★' : '☆'}</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  card: { flexDirection: 'row', alignItems: 'center', padding: 12, borderRadius: 10, backgroundColor: '#fff' },
  pressable: { flex: 1, flexDirection: 'row', alignItems: 'center' },
  photo: { width: 56, height: 56, borderRadius: 8, marginRight: 12 },
  body: { flex: 1 },
  name: { fontSize: 16, fontWeight: '600' },
  category: { fontSize: 13, color: '#666' },
  favourite: { fontSize: 20, marginLeft: 8 },
});

A caller can now both override the card’s outer margin/width from a parent FlatList and measure it via the ref, both fully type-checked:

const cardRef = useRef<ViewInstance>(null);

<PlaceCard ref={cardRef} place={place} style={{ marginBottom: 12 }} onPress={openDetails} onToggleFavourite={toggle} />

The forwardRef wrapper this replaces still works and is what a codebase on React 18 or earlier must use, but React 19 does not need it for a function component — see Refs and the DOM in the React reference.