TypeScript in RN Basics
10 examples to get you started with TypeScript in RN - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with TypeScript in RN - 7 basic and 3 intermediate.
Expo projects scaffold with TypeScript by default. Start from the blank TypeScript template so tsconfig.json, Metro, and the SDK pin are already aligned.
npx create-expo-app@latest MyTypedApp --template blank-typescript
cd MyTypedAppConfirm the SDK and React pins in package.json:
{
"dependencies": {
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Conventions used throughout:
.tsx. Plain .ts is for schemas, API clients, and shared types.strict: true in tsconfig.json - the default when you extend Expo's base config.react-native (StyleProp, ViewStyle, GestureResponderEvent) instead of redeclaring them.Tooling: Run
npx tsc --noEmitin CI to catch type errors before a native build. Pair it with ESLint's@typescript-eslintrules for unused variables and unsafeany.
Extend Expo's published base config instead of hand-rolling compiler options that drift from the SDK.
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}expo/tsconfig.base ships with Expo SDK 57 and already sets jsx, moduleResolution, and paths Metro expectsstrict: true enables strictNullChecks, noImplicitAny, and related flags - catch undefined route params and untyped API payloads early.expo/types/**/*.ts include picks up generated route types when typed routes are enabledstrict to silence errors - fix the boundary (props, params, fetch) or add a narrow escape hatch with a commentRelated: Gradual Typing in Brownfield Apps - tightening strictness without blocking feature work | Expo Router Typed Routes - generated types land in
.expo/types
Define the shape of a component's props as a TypeScript interface and destructure in the parameter list.
import { View, Text, StyleSheet } from "react-native";
interface ProfileHeaderProps {
name: string;
subtitle: string;
}
function ProfileHeader({ name, subtitle }: ProfileHeaderProps) {
return (
<View style={styles.header}>
<Text style={styles.name}>{name}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
);
}
const styles = StyleSheet.create({
header: { padding: 16, alignItems: "center" },
name: { fontSize: 20, fontWeight: "600" },
subtitle: { fontSize: 14, color: "#6b7280", marginTop: 4 },
});<ComponentName>Props - easy to find with search and safe to extend laterinterface for component props; declaration merging makes extending straightforwardReact.FC - it adds an implicit children prop you often do not want and is falling out of favor in 2026extends or Pick from itRelated: Typing Components & Props - generics, discriminated unions, and reusable prop contracts
Mark props optional with ?, then supply defaults in the destructure so callers can omit them safely.
import { Pressable, Text, StyleSheet } from "react-native";
interface ActionButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onPress: () => void;
}
function ActionButton({
label,
variant = "primary",
disabled = false,
onPress,
}: ActionButtonProps) {
return (
<Pressable
style={[styles.base, variant === "primary" ? styles.primary : styles.secondary]}
disabled={disabled}
onPress={onPress}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
base: { paddingHorizontal: 20, paddingVertical: 12, borderRadius: 8 },
primary: { backgroundColor: "#2563eb" },
secondary: { backgroundColor: "#e5e7eb" },
label: { fontWeight: "600", color: "#fff" },
});variant?: ... makes the prop optional; TypeScript adds undefined to its typevariant = "primary") fill in when the caller omits the prop"primary" | "secondary") restricts callers to valid options - autocomplete works in the editordefaultProps - it is deprecated for function components in React 19Related: Typing Components & Props - variant props and discriminated unions for mode-specific fields
When a component accepts a style override, type it with StyleProp so callers can pass a single object or an array.
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from "react-native";
interface CardProps {
title: string;
style?: StyleProp<ViewStyle>;
}
function Card({ title, style }: CardProps) {
return (
<View style={[styles.card, style]}>
<Text style={styles.title}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
margin: 16,
padding: 20,
borderRadius: 12,
backgroundColor: "#fff",
},
title: { fontSize: 18, fontWeight: "600" },
});
// Usage - single object or array both type-check
// <Card title="Hello" style={{ marginTop: 24 }} />
// <Card title="Hello" style={[styles.card, isActive && styles.active]} />StyleProp<ViewStyle> accepts a style object, an array of styles, false, or undefined - matching how RN merges style arraysTextStyle and ImageStyle for text and image wrappers respectively; do not reuse ViewStyle on Text[styles.card, style] so the caller's override wins without replacing your base layoutPressable, type style as StyleProp<ViewStyle> | ((state) => StyleProp<ViewStyle>) when you need pressed-state stylingRelated: Utility Types for RN -
StyleProp,ComponentProps, and style-safe helpers | Styling Basics -StyleSheet.createand the RN styling model
Let inference do the work, and add an explicit generic when the initial value does not carry the full type.
import { useState } from "react";
import { View, Text, ActivityIndicator, StyleSheet } from "react-native";
interface User {
id: string;
name: string;
}
export default function UserPanel() {
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
if (loading) {
return <ActivityIndicator style={styles.centered} />;
}
return (
<View style={styles.centered}>
<Text>Count: {count}</Text>
<Text>{user?.name ?? "No user loaded"}</Text>
</View>
);
}
const styles = StyleSheet.create({
centered: { flex: 1, justifyContent: "center", alignItems: "center" },
});useState(initial) has enough info - useState(0) is already numberuseState<User | null>(null) when the value could later be richer - otherwise TypeScript pins it to nullUser | null make the "not loaded yet" state explicit - consumers must narrow before using fieldsRelated: Props, State & Re-renders on Mobile - when state updates trigger native re-renders
Use React Native's exported event types so handler parameters stay typed without any.
import { useState } from "react";
import {
Pressable,
Text,
StyleSheet,
type GestureResponderEvent,
} from "react-native";
export default function LikeButton() {
const [liked, setLiked] = useState(false);
const handlePress = (event: GestureResponderEvent) => {
setLiked((prev) => !prev);
console.log("pressed at", event.nativeEvent.pageX, event.nativeEvent.pageY);
};
return (
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.pressed]}
onPress={handlePress}
>
<Text style={styles.label}>{liked ? "Liked" : "Like"}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: { padding: 12, borderRadius: 8, backgroundColor: "#2563eb" },
pressed: { opacity: 0.85 },
label: { color: "#fff", fontWeight: "600" },
});GestureResponderEvent is the standard type for onPress, onPressIn, and onPressOut on Pressable and TouchableOpacityevent.nativeEvent - the shape is stable across iOS and AndroidonPress's signatureonLongPress the same way; for TextInput, use NativeSyntheticEvent<TextInputChangeEventData>Related: Typing Components & Props - callback props and event handler contracts
Derive new types from existing ones instead of writing duplicate interfaces that drift.
interface Device {
id: string;
name: string;
platform: "ios" | "android";
lastSeenAt: string;
}
type DeviceSummary = Pick<Device, "id" | "name">;
type NewDevice = Omit<Device, "id" | "lastSeenAt">;
type DevicePatch = Partial<Omit<Device, "id">>;Pick<T, K> keeps only the listed keys; Omit<T, K> removes them - compose both to express API shapesPartial<T> makes every property optional; handy for PATCH endpoints and form draftsRequired<T> and Readonly<T> round out the most-used utilities for immutable config objectsRelated: Utility Types for RN -
ComponentProps,StyleProp, and RN-specific helpers
Type search params at the screen boundary so missing or mistyped keys fail at compile time.
import { View, Text, StyleSheet } from "react-native";
import { useLocalSearchParams } from "expo-router";
type ProductParams = {
id: string;
preview?: string;
};
export default function ProductScreen() {
const { id, preview } = useLocalSearchParams<ProductParams>();
return (
<View style={styles.screen}>
<Text style={styles.title}>Product {id}</Text>
{preview ? <Text style={styles.badge}>Preview mode</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 24 },
title: { fontSize: 22, fontWeight: "600" },
badge: { marginTop: 8, color: "#2563eb" },
});useLocalSearchParams<Params>() types the return value - optional keys use ? in the params typeNumber(id) or validate with Zod before useapp/product/[id].tsx, keep param names aligned with the file name (id matches [id])experiments.typedRoutes in app.json) for links that stay in sync with the file treeRelated: Typing Navigation & Route Params - React Navigation stacks and param validation | Expo Router Typed Routes - generated
Hreftypes and keeping them current
Guarantee at runtime that API responses match the TypeScript type you ship to screens.
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json: unknown = await res.json();
return UserSchema.parse(json);
}Promise<User> is a lie if you never validate - mobile APIs change without warningunknown until it passes the schema - no accidental any leaking into UI componentsqueryFnRelated: Typing API Responses & Zod - error shapes, schema composition, and offline caches
Reuse a built-in component's prop surface when wrapping Pressable, Text, or TextInput.
import {
Pressable,
Text,
StyleSheet,
type ComponentProps,
} from "react-native";
type PressableProps = ComponentProps<typeof Pressable>;
interface LinkButtonProps extends Omit<PressableProps, "children"> {
label: string;
}
function LinkButton({ label, style, ...pressableProps }: LinkButtonProps) {
return (
<Pressable style={[styles.link, style]} {...pressableProps}>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
link: { paddingVertical: 8 },
label: { color: "#2563eb", fontWeight: "600" },
});ComponentProps<typeof Pressable> captures every prop Pressable accepts - onPress, disabled, accessibilityRole, and moreOmit<..., "children"> removes props you replace with your own API (label instead of free-form children)...pressableProps last so callers can override defaults without re-listing every RN propTextInput, ScrollView, and third-party components that forward refsRelated: Utility Types for RN -
ComponentProps,StyleProp, and composition patterns | Typing Native Module APIs - bridging untyped native returns into safe TS boundaries
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 19, 2026