React Native and TypeScript in 2026: Type-Safe Architecture and Interview Questions

Build type-safe React Native apps with TypeScript, Codegen, TurboModules, and the Strict TypeScript API. Covers architecture patterns, typed navigation, and interview questions for 2026.

React Native TypeScript type-safe architecture with code and mobile devices

React Native TypeScript type safety has matured significantly by mid-2026, with version 0.86 shipping React 19.1, the Strict TypeScript API, and a fully bridgeless architecture powered by Codegen. TypeScript is no longer optional glue between JavaScript and native code — it drives the entire contract from component props to TurboModule interfaces, catching mismatches at build time rather than in production crash logs.

What changed in 2026

React Native 0.80 introduced the opt-in Strict TypeScript API, 0.82 permanently removed the legacy bridge, and 0.85 dropped the last interop layer. As of 0.86 (June 2026), every new project starts fully bridgeless with TypeScript types generated directly from source code.

Why TypeScript Is Now the Default for React Native Projects

Every npx react-native init invocation since version 0.76 scaffolds a TypeScript project. But the real shift happened at the native boundary. Before Codegen, developers wrote manual type assertions when crossing from JavaScript to Objective-C or Kotlin — a stringly-typed contract that broke silently at runtime. Codegen reads TypeScript spec files and generates C++, Objective-C++, and Java/Kotlin interfaces automatically. If the TypeScript spec declares a method returning number, the generated native interface enforces that constraint at compile time.

The React Native documentation covers the baseline setup, but the type-safe patterns that matter in production go further: typed navigation stacks, generic API hooks, discriminated unions for state machines, and Codegen-driven TurboModule specs.

Setting Up the Strict TypeScript API

The Strict TypeScript API, introduced in React Native 0.80, restricts the public API surface to types generated directly from source code. This prevents accidental reliance on internal modules that could break between minor versions.

tsconfig.jsonjson
{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "types": ["react-native/types/strict"]
  },
  "extends": "@react-native/typescript-config/tsconfig.json"
}

With this configuration, importing anything from a subpath like react-native/Libraries/Text/Text triggers a type error. All imports must come from the root react-native package, aligning with the deep-import deprecation enforced since 0.80.

Type-Safe Navigation with React Navigation 7

React Navigation 7.x provides first-class TypeScript support. The key pattern: define a RootStackParamList type that maps every screen name to its expected params, then thread that type through navigators and screen components.

navigation/types.tstypescript
export type RootStackParamList = {
  Home: undefined;
  Profile: { userId: string };
  Settings: undefined;
  ArticleDetail: { articleId: string; source: 'feed' | 'search' };
};

export type AppTabParamList = {
  Dashboard: undefined;
  Explore: { category?: string };
  Notifications: undefined;
};

Screen components then receive typed props without manual casting:

screens/ArticleDetailScreen.tsxtypescript
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { RootStackParamList } from '../navigation/types';

type Props = NativeStackScreenProps<RootStackParamList, 'ArticleDetail'>;

export function ArticleDetailScreen({ route, navigation }: Props) {
  // route.params.articleId is string — guaranteed by the type
  // route.params.source is 'feed' | 'search' — no runtime check needed
  const { articleId, source } = route.params;

  // navigation.navigate('Profile', { userId: '123' }) — type-checked
  // navigation.navigate('Profile', {}) — compile error: missing userId
  return (
    <ArticleView id={articleId} referrer={source} />
  );
}

This eliminates an entire class of runtime errors — navigating to a screen with wrong or missing params fails at build time.

useNavigation hook typing

For components that aren't direct screen children, use useNavigation<NativeStackNavigationProp<RootStackParamList>>() to get the same type safety without prop drilling.

Building a Type-Safe TurboModule with Codegen

TurboModules replace the old Native Modules system. The TypeScript spec file serves as the single source of truth — Codegen generates the native interfaces from it. If the spec and native implementation diverge, the build fails.

specs/NativeDeviceInfo.tstypescript
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  getDeviceModel(): string;
  getBatteryLevel(): Promise<number>;
  getStorageInfo(): Promise<{
    totalBytes: number;
    freeBytes: number;
    usedPercentage: number;
  }>;
  onBatteryChange(callback: (level: number) => void): void;
}

export default TurboModuleRegistry.getEnforcing<Spec>('DeviceInfo');

Running npx react-native codegen generates the corresponding C++, Objective-C++, and Java interfaces. The native implementation must match every method signature exactly. For example, getStorageInfo must return an object with three numeric fields — returning a different shape causes a compile-time error on the native side.

android/app/src/main/java/com/app/DeviceInfoModule.ktkotlin
class DeviceInfoModule(reactContext: ReactApplicationContext) :
    NativeDeviceInfoSpec(reactContext) {

    // Return type enforced by generated NativeDeviceInfoSpec
    override fun getDeviceModel(): String {
        return Build.MODEL
    }

    override fun getBatteryLevel(): Promise<Double> {
        val bm = reactContext.getSystemService(Context.BATTERY_SERVICE)
            as BatteryManager
        val level = bm.getIntProperty(
            BatteryManager.BATTERY_PROPERTY_CAPACITY
        ).toDouble()
        return Promise.resolve(level)
    }
}

This approach eliminates the ReadableMap and NSDictionary parsing that caused silent type coercion bugs in the old architecture.

Ready to ace your React Native interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Generic Data Fetching with Typed API Hooks

A reusable typed hook pattern avoids duplicating fetch logic across screens while preserving full type inference:

hooks/useApiQuery.tstypescript
import { useQuery, UseQueryOptions } from '@tanstack/react-query';

interface ApiResponse<T> {
  data: T;
  meta: { page: number; totalPages: number };
}

export function useApiQuery<T>(
  key: readonly string[],
  endpoint: string,
  options?: Omit<UseQueryOptions<ApiResponse<T>>, 'queryKey' | 'queryFn'>
) {
  return useQuery<ApiResponse<T>>({
    queryKey: key,
    queryFn: async () => {
      const response = await fetch(`${API_BASE}${endpoint}`);
      if (!response.ok) throw new ApiError(response.status);
      return response.json() as Promise<ApiResponse<T>>;
    },
    ...options,
  });
}

// Usage — T is inferred as Article[]
interface Article {
  id: string;
  title: string;
  publishedAt: string;
}

const { data, isLoading } = useApiQuery<Article[]>(
  ['articles', 'latest'],
  '/articles?sort=latest'
);
// data.data is Article[] — fully typed
// data.meta.totalPages is number

The generic parameter T flows through the entire chain: from the hook call site, through the query function, to the component that consumes the result. No as casts, no any types.

Discriminated Unions for State Machines

Complex screen states — loading, error, empty, loaded — are best modeled as discriminated unions rather than a bag of optional fields:

types/screen-state.tstypescript
type ScreenState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'error'; error: string; retryCount: number }
  | { status: 'empty'; message: string }
  | { status: 'loaded'; data: T; refreshedAt: Date };

// components/DataScreen.tsx
function renderContent<T>(state: ScreenState<T>, renderItem: (data: T) => ReactNode) {
  switch (state.status) {
    case 'idle':
      return null;
    case 'loading':
      return <LoadingSpinner />;
    case 'error':
      // state.error is string here — TypeScript narrows automatically
      return <ErrorBanner message={state.error} retries={state.retryCount} />;
    case 'empty':
      return <EmptyState message={state.message} />;
    case 'loaded':
      // state.data is T — fully typed
      return renderItem(state.data);
  }
}

This pattern makes impossible states unrepresentable. A loading state cannot accidentally carry stale data, and an error state always includes context for debugging.

Avoid partial state objects

A common anti-pattern: { isLoading: boolean; error?: string; data?: T }. This allows states like { isLoading: true, error: 'fail', data: [...] } — three contradictory signals at once. Discriminated unions prevent this at the type level.

React Native TypeScript Interview Questions

These questions reflect what senior mobile engineering teams ask in 2026 interviews, now that the New Architecture and TypeScript are standard.

How does Codegen enforce type safety across the JavaScript-native boundary?

Codegen reads TypeScript (or Flow) spec files and generates C++, Objective-C++, and Java/Kotlin interface code. The generated native interfaces enforce the exact method signatures, parameter types, and return types defined in the spec. If the native implementation deviates — returning Int where the spec declares Double, or omitting a field from a struct — the native compiler rejects the build. This moves type errors from runtime crashes to build-time failures.

What is the Strict TypeScript API and why does it matter?

Introduced in React Native 0.80, the Strict TypeScript API generates types directly from React Native's source code rather than maintaining hand-written .d.ts files. It restricts imports to the root react-native package, deprecating deep imports. This defines a stable public API surface — internal refactors cannot break consumer code if consumers only use strict types. Enable it via "types": ["react-native/types/strict"] in tsconfig.json.

How do you type React Navigation params across nested navigators?

Define a ParamList type per navigator and compose them using NavigatorScreenParams. For a tab navigator nested inside a stack, the stack's param list references the tab's: type RootStack = { Main: NavigatorScreenParams<TabParamList>; Modal: { id: string } }. Every navigate() call is then type-checked through the full nesting hierarchy, catching wrong screen names or missing params at compile time.

What problem do discriminated unions solve in React Native state management?

Discriminated unions model mutually exclusive states (loading, error, loaded) as separate branches of a union type, keyed by a status field. TypeScript narrows the type in each branch of a switch statement, so accessing state.data is only possible when state.status === 'loaded'. This prevents impossible states like a loading indicator showing alongside error data — a class of bugs that optional fields and boolean flags cannot prevent.

Explain the difference between TurboModules and the old Native Modules system.

Native Modules communicated over the async bridge, serializing all data to JSON. TurboModules use JSI (JavaScript Interface) for synchronous, direct C++ calls — no serialization overhead. They also load lazily (on first use instead of app startup, reducing cold start time) and use Codegen to generate type-safe interfaces from TypeScript specs. The old system relied on ReadableMap / NSDictionary parsing with runtime type coercion; TurboModules enforce types at compile time.

For more React Native interview questions, the native modules deep-dive covers JSI, Fabric, and bridgeless architecture in detail.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Conclusion

  • The Strict TypeScript API (0.80+) restricts imports to the stable public surface, preventing breakage from internal changes — enable it in every new project
  • Codegen generates native interfaces from TypeScript spec files, moving type errors from runtime crashes to build-time failures across the JS-native boundary
  • Typed navigation params via RootStackParamList and NativeStackScreenProps catch wrong screen names and missing params before the app runs
  • Discriminated unions model screen states as mutually exclusive branches, making impossible states unrepresentable at the type level
  • TurboModules with typed specs replace the old ReadableMap / NSDictionary parsing, enforcing full type safety from JavaScript through C++ to platform-native code
  • Generic API hooks with TanStack Query preserve type inference from endpoint to component without manual casts

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#react-native
#typescript
#mobile-development
#new-architecture
#turbomodules

Share

Related articles