Vue NativeVue Native
Guide
Components
Composables
Navigation
Architecture
  • iOS
  • Android
  • macOS
GitHub
Guide
Components
Composables
Navigation
Architecture
  • iOS
  • Android
  • macOS
GitHub
  • Getting Started

    • Introduction
    • Installation
    • Your First App
    • Project Structure
  • Core Concepts

    • Components
    • Styling
    • Theming
    • TypeScript
    • Navigation
    • Navigation Components
    • Native Modules
    • Native Code Blocks
    • Hot Reload
  • UI Patterns

    • Forms and v-model
    • Shared Element Transitions
    • Teleport
    • Custom Native Components (escape hatch)
  • Quality & Debugging

    • Testing
    • Debugging
    • Error Handling
  • Platform Hardening

    • Security
    • Accessibility
    • Performance
  • Integration Guides

    • State Management
    • Deep Linking & Universal Links
    • State Persistence
    • Push Notifications
    • Error Reporting & Monitoring
  • Tooling

    • Managed Workflow
    • VS Code Extension
    • Neovim Plugin
  • Building & Releasing

    • Building Native Apps
    • Deployment & App Store Submission
  • Reference

    • Upgrade Guide
    • Migrating from React Native
    • Known Limitations & Platform Differences
    • Troubleshooting

TypeScript

Vue Native is written in TypeScript and provides full type safety for components, composables, styles, and configuration. This guide covers the type system and how to get the most out of it in your projects.

Configuration

This is the tsconfig.json that vue-native create generates (recommended):

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "lib": ["ES2020"],
    "skipLibCheck": true,
    "types": [],
    "paths": {
      "vue": ["./node_modules/@thelacanians/vue-native-runtime/dist/index.d.ts"]
    }
  },
  "include": ["app/**/*", "env.d.ts"]
}

Key settings explained:

OptionValueReason
targetES2020JavaScriptCore on iOS 16+ supports ES2020 features.
moduleESNextVite handles module bundling; keep source as ESM.
moduleResolutionbundlerMatches Vite's resolution strategy for imports.
stricttrueEnables all strict type-checking options.
jsxpreserveAllows Vue SFC <script setup lang="ts"> to work correctly.
lib["ES2020"]Matches JavaScriptCore/V8. Do not add DOM — there is no browser.
skipLibChecktrueVue and Vite ship .d.ts files that mention Element/WebSocket. Skip those, do not add DOM libs.
types[]Avoids pulling in ambient @types packages; project ambient declarations live in env.d.ts.
paths.vueSee aboveResolves vue imports to the native runtime so the Composition API types come from the custom-renderer build.

Editor setup

The runtime augments Vue's GlobalComponents interface with every built-in component (VView, VText, VButton, ...), so any editor running the Vue language server (Volar) resolves the globally registered components in your templates with no extra configuration: tag autocomplete, prop typing, and createStyleSheet style checking all work out of the box.

VS Code

Install the Vue (Official) extension. That is the only setup needed.

Neovim

Run the Vue language server alongside your TypeScript server, for example with nvim-lspconfig:

-- Vue single-file components (templates, script blocks)
vim.lsp.enable('vueols')
-- TypeScript files
vim.lsp.enable('ts_ls')

Both servers must be able to see the project's node_modules (with vue and typescript installed), so open Neovim at the project root.

Type-checking templates in CI

Plain tsc --noEmit does not check .vue templates. Use vue-tsc — the same engine as the editor LSP — for CI type-checking:

bun add -d vue-tsc
{
  "scripts": {
    "typecheck": "vue-tsc --noEmit"
  }
}

Style Types

Vue Native exports three style interfaces from @thelacanians/vue-native-runtime. These types map directly to native layout and rendering properties.

ViewStyle

The base style type for all container components (VView, VScrollView, VSafeArea, etc.). Includes layout (Flexbox), spacing, borders, backgrounds, and transforms.

import type { ViewStyle } from '@thelacanians/vue-native-runtime'

const container: ViewStyle = {
  // Layout
  flex: 1,
  flexDirection: 'column',
  justifyContent: 'center',
  alignItems: 'center',

  // Spacing
  padding: 20,
  margin: 10,
  gap: 12,

  // Appearance
  backgroundColor: '#f5f5f5',
  borderRadius: 12,
  borderWidth: 1,
  borderColor: '#e0e0e0',

  // Shadow (iOS)
  shadowColor: '#000',
  shadowOffset: { width: 0, height: 2 },
  shadowOpacity: 0.1,
  shadowRadius: 4,

  // Elevation (Android)
  elevation: 3,

  // Dimensions
  width: '100%',
  height: 200,
  minHeight: 100,
  maxWidth: 400,

  // Positioning
  position: 'relative',
  overflow: 'hidden',

  // Opacity
  opacity: 1,
}

TextStyle

Extends ViewStyle with font and text-specific properties. Used by VText and text-containing components.

import type { TextStyle } from '@thelacanians/vue-native-runtime'

const heading: TextStyle = {
  // All ViewStyle properties are available, plus:
  fontSize: 28,
  fontWeight: 'bold',
  fontStyle: 'italic',
  fontFamily: 'System',
  color: '#1a1a1a',
  textAlign: 'center',
  textDecorationLine: 'underline',
  textTransform: 'uppercase',
  lineHeight: 34,
  letterSpacing: 0.5,
}

ImageStyle

Extends ViewStyle with image-specific properties. Used by VImage.

import type { ImageStyle } from '@thelacanians/vue-native-runtime'

const avatar: ImageStyle = {
  // All ViewStyle properties are available, plus:
  resizeMode: 'cover', // 'cover' | 'contain' | 'stretch' | 'center'
  tintColor: '#007AFF',
  width: 48,
  height: 48,
  borderRadius: 24,
}

Using Style Types in Components

You can type style props and variables to catch errors at compile time:

<script setup lang="ts">
import type { ViewStyle, TextStyle } from '@thelacanians/vue-native-runtime'
import { VView, VText } from '@thelacanians/vue-native-runtime'

// TypeScript will error if you use an invalid property
const card: ViewStyle = {
  padding: 16,
  backgroundColor: '#fff',
  borderRadius: 12,
  shadowColor: '#000',
  shadowOpacity: 0.1,
  shadowRadius: 8,
  shadowOffset: { width: 0, height: 2 },
}

const title: TextStyle = {
  fontSize: 20,
  fontWeight: '600',
  color: '#333',
  marginBottom: 8,
}

const subtitle: TextStyle = {
  fontSize: 14,
  color: '#888',
}
</script>

<template>
  <VView :style="card">
    <VText :style="title">Card Title</VText>
    <VText :style="subtitle">A short description</VText>
  </VView>
</template>

createStyleSheet

The createStyleSheet utility provides a type-safe, organized way to define groups of styles — similar to React Native's StyleSheet.create. It validates that each style value conforms to ViewStyle, TextStyle, or ImageStyle and returns a typed object.

import { createStyleSheet } from '@thelacanians/vue-native-runtime'

const styles = createStyleSheet({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#fafafa',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#1a1a1a',
    marginBottom: 12,
  },
  avatar: {
    width: 64,
    height: 64,
    borderRadius: 32,
    resizeMode: 'cover',
  },
  button: {
    padding: 14,
    backgroundColor: '#007AFF',
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
})

Then use it in your template:

<template>
  <VView :style="styles.container">
    <VImage source="avatar.png" :style="styles.avatar" />
    <VText :style="styles.title">Hello, Vue Native</VText>
    <VButton :onPress="handlePress" :style="styles.button">
      <VText :style="styles.buttonText">Get Started</VText>
    </VButton>
  </VView>
</template>

Benefits of createStyleSheet:

  • Type safety — invalid property names or values are caught at compile time.
  • Autocomplete — your editor suggests valid style properties as you type.
  • Organization — keeps styles separate from template logic.
  • Reuse — export and share stylesheets across components.

Component Prop Types

Every Vue Native component has fully typed props. Your editor will provide autocomplete and type checking for all prop values.

<script setup lang="ts">
import {
  ref,
  VView,
  VText,
  VInput,
  VButton,
  VSwitch,
  VImage,
  VSlider,
} from '@thelacanians/vue-native-runtime'

const text = ref('')
const isEnabled = ref(false)
const sliderValue = ref(0.5)
</script>

<template>
  <!-- VInput: modelValue (v-model), placeholder, secureTextEntry, keyboardType,
       returnKeyType, autoCapitalize, autoCorrect, maxLength, multiline -->
  <VInput
    v-model="text"
    placeholder="Type here..."
    :maxLength="100"
    keyboardType="email-address"
  />

  <!-- VSwitch: modelValue (v-model), disabled, onTintColor, thumbTintColor -->
  <VSwitch
    v-model="isEnabled"
    onTintColor="#007AFF"
    thumbTintColor="#fff"
  />

  <!-- VImage: source is an object — { uri } for remote, { asset } for bundled -->
  <VImage
    :source="{ asset: 'logo' }"
    resizeMode="contain"
    :style="{ width: 120, height: 120 }"
  />

  <!-- VSlider: modelValue (v-model), min, max -->
  <VSlider
    v-model="sliderValue"
    :min="0"
    :max="1"
  />
</template>

TypeScript will flag errors like passing a number where a string is expected, or misspelling a prop name. Note that loosely-typed props such as keyboardType are declared as plain string on the component, so invalid values are not caught at compile time — check the component reference pages for the accepted values.

Composable Return Types

All composables return properly typed refs, reactive objects, and functions. This gives you autocomplete and type checking without manual annotations.

<script setup lang="ts">
import { useDeviceInfo, usePlatform } from '@thelacanians/vue-native-runtime'
import { useRouter, useRoute } from '@thelacanians/vue-native-navigation'

// useDeviceInfo returns typed refs. Values are empty/0 until the native
// DeviceInfo module resolves on mount — gate on `isLoaded`.
const {
  model,          // Ref<string>  — e.g. "iPhone15,2", "Pixel 7"
  systemName,     // Ref<string>  — e.g. "iOS", "Android", "macOS"
  systemVersion,  // Ref<string>  — e.g. "17.4"
  name,           // Ref<string>  — user-assigned device name
  screenWidth,    // Ref<number>
  screenHeight,   // Ref<number>
  scale,          // Ref<number>  — pixel density (3 for @3x Retina)
  isLoaded,       // Ref<boolean>
  fetchInfo,      // () => Promise<void>
} = useDeviceInfo()

// Platform detection lives in usePlatform(), not useDeviceInfo().
const {
  platform,  // 'ios' | 'android' | 'macos'
  isIOS,
  isAndroid,
  isMacOS,
  isApple,   // iOS or macOS
  isDesktop, // macOS
  isMobile,  // iOS or Android
} = usePlatform()

// useRouter returns the RouterInstance. Routes are addressed by NAME — there is
// no path/URL string form. Every navigation method returns a Promise.
const router = useRouter()
// router.push(name: string, params?: RouteParams, options?: NavigateOptions): Promise<void>
// router.navigate(...) — same signature; `push` is the preferred alias
// router.replace(name: string, params?: RouteParams): Promise<void>
// router.reset(name: string, params?: RouteParams): Promise<void>
// router.goBack(): Promise<void>   — `pop()` is the preferred alias
// router.canGoBack: ComputedRef<boolean>

// useRoute returns a ComputedRef<RouteLocation>.
const route = useRoute()
// route.value.name: string
// route.value.params: RouteParams      // Record<string, unknown>
// route.value.options: RouteOptions    // { title?, headerShown?, animation?, ... }
// There is no `path` and no `query` — deep links are mapped to route names by
// the router's `linking` config, and any values they carry arrive in `params`.
</script>

No back(), no route.path

goBack() (alias pop()) is the only way back — there is no router.back(). And because RouteLocation is { name, params, options }, reading route.value.path or route.value.query is undefined at runtime and a type error under strict. See Navigation and useDeviceInfo.

Typing Custom Composables

When building your own composables, leverage Vue Native's types:

import {
  ref,
  onMounted,
  useHttp,
  type Ref,
} from '@thelacanians/vue-native-runtime'

interface User {
  id: number
  name: string
  email: string
}

interface UseUsersReturn {
  users: Ref<User[]>
  loading: Ref<boolean>
  error: Ref<string | null>
  refresh: () => Promise<void>
}

export function useUsers(): UseUsersReturn {
  const users = ref<User[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  const { get } = useHttp()

  async function refresh() {
    loading.value = true
    error.value = null
    try {
      const response = await get<User[]>('https://api.example.com/users')
      // `data` is `T | undefined` — 204/205 responses legitimately have no body.
      users.value = response.data ?? []
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }

  onMounted(refresh)

  return { users, loading, error, refresh }
}

Typed Navigation

The navigation package exports types for route configuration and navigation guards:

import { createRouter } from '@thelacanians/vue-native-navigation'
import type { RouteConfig, NavigationGuard } from '@thelacanians/vue-native-navigation'
import HomeScreen from './screens/Home.vue'
import ProfileScreen from './screens/Profile.vue'
import LoginScreen from './screens/Login.vue'

// Route configuration is fully typed. Routes are keyed by NAME — there is no
// `path` field (deep-link paths are configured separately via `linking`).
const routes: RouteConfig[] = [
  { name: 'Home', component: HomeScreen },
  { name: 'Profile', component: ProfileScreen },
  { name: 'Login', component: LoginScreen },
]

// Navigation guards are typed. `to`/`from` are RouteEntry objects, so the route
// name lives at `to.config.name`. Returning a route name redirects there;
// returning false cancels navigation.
const authGuard: NavigationGuard = (to, from) => {
  const isAuthenticated = checkAuth()
  if (to.config.name !== 'Login' && !isAuthenticated) {
    return 'Login' // redirect by route NAME
  }
}

// createRouter accepts a RouteConfig[] or a { routes, linking } options object
const router = createRouter(routes)
router.beforeEach(authGuard)

export default router

Typing Event Handlers

Event callbacks from components are typed. Use them directly or extract the types:

<script setup lang="ts">
import { ref } from '@thelacanians/vue-native-runtime'
import { VButton, VInput, VList, VText, VView } from '@thelacanians/vue-native-runtime'

interface Item {
  id: string
  name: string
}

const text = ref('')
const items = ref<Item[]>([])

// VButton's onPress receives no arguments
function handlePress(): void {
  console.log('Button pressed')
}

// VInput is v-model based: it emits `update:modelValue` with the new string.
// There is no `onChangeText` prop.
function handleTextChange(next: string): void {
  text.value = next
}

// VList's `endReached` event receives no arguments
function handleEndReached(): void {
  console.log('Load more items')
}
</script>

<template>
  <VButton :onPress="handlePress">
    <VText>Tap Me</VText>
  </VButton>

  <VInput
    :modelValue="text"
    placeholder="Type..."
    @update:modelValue="handleTextChange"
  />

  <!-- VList renders rows through its `#item` scoped slot. It has no
       `renderItem` prop — that is the React Native (and VFlatList) API. -->
  <VList
    :data="items"
    :keyExtractor="(item: Item) => item.id"
    @endReached="handleEndReached"
  >
    <template #item="{ item }">
      <VView :style="{ padding: 12 }">
        <VText>{{ (item as Item).name }}</VText>
      </VView>
    </template>
  </VList>
</template>

Generic Components

When building reusable components, use TypeScript generics for type-safe data flow. Because VList renders rows through a scoped slot rather than a render prop, a generic wrapper forwards that slot and narrows item back to T:

<!-- TypedList.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
import { VList } from '@thelacanians/vue-native-runtime'

defineProps<{
  items: T[]
  keyExtractor?: (item: T, index: number) => string
}>()

function defaultKeyExtractor(item: T): string {
  return String(item.id)
}
</script>

<template>
  <VList
    :data="items"
    :keyExtractor="keyExtractor ?? defaultKeyExtractor"
    :style="{ flex: 1 }"
  >
    <template #item="{ item, index }">
      <slot name="item" :item="item as T" :index="index" />
    </template>
  </VList>
</template>

Usage:

<script setup lang="ts">
import TypedList from './TypedList.vue'
import { VText, VView } from '@thelacanians/vue-native-runtime'

interface Task {
  id: number
  title: string
  done: boolean
}

const tasks: Task[] = [
  { id: 1, title: 'Learn Vue Native', done: false },
  { id: 2, title: 'Build an app', done: false },
]
</script>

<template>
  <!-- `item` is inferred as Task because TypedList is generic over T -->
  <TypedList :items="tasks">
    <template #item="{ item, index }">
      <VView :style="{ padding: 12, flexDirection: 'row' }">
        <VText>{{ index + 1 }}. {{ item.title }}</VText>
      </VView>
    </template>
  </TypedList>
</template>

Type-Safe Styles with Themes

Combine TypeScript with a theme object for consistent, type-safe styling across your app:

// theme.ts
export const theme = {
  colors: {
    primary: '#007AFF',
    secondary: '#5856D6',
    background: '#F2F2F7',
    surface: '#FFFFFF',
    text: '#1C1C1E',
    textSecondary: '#8E8E93',
    error: '#FF3B30',
    success: '#34C759',
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32,
  },
  fontSize: {
    body: 16,
    caption: 12,
    heading: 24,
    title: 32,
  },
  borderRadius: {
    sm: 6,
    md: 12,
    lg: 20,
    full: 9999,
  },
} as const

// The type is inferred with literal types thanks to `as const`
export type Theme = typeof theme
// styles.ts
import { createStyleSheet } from '@thelacanians/vue-native-runtime'
import { theme } from './theme'

export const commonStyles = createStyleSheet({
  screen: {
    flex: 1,
    backgroundColor: theme.colors.background,
    padding: theme.spacing.md,
  },
  card: {
    backgroundColor: theme.colors.surface,
    borderRadius: theme.borderRadius.md,
    padding: theme.spacing.md,
    shadowColor: '#000',
    shadowOpacity: 0.05,
    shadowRadius: 8,
    shadowOffset: { width: 0, height: 2 },
  },
  heading: {
    fontSize: theme.fontSize.heading,
    fontWeight: 'bold',
    color: theme.colors.text,
    marginBottom: theme.spacing.sm,
  },
  body: {
    fontSize: theme.fontSize.body,
    color: theme.colors.text,
    lineHeight: 22,
  },
  primaryButton: {
    backgroundColor: theme.colors.primary,
    borderRadius: theme.borderRadius.sm,
    padding: theme.spacing.md,
    alignItems: 'center',
  },
  primaryButtonText: {
    color: '#fff',
    fontSize: theme.fontSize.body,
    fontWeight: '600',
  },
})

Best Practices

  1. Enable strict mode. Set "strict": true in your tsconfig.json. This catches null errors, implicit any types, and other common mistakes.

  2. Use createStyleSheet over inline objects. You get autocomplete, compile-time validation, and cleaner templates.

  3. Type your composable return values. Explicitly defining return interfaces makes composables easier to consume and documents their API.

  4. Leverage as const for constants. Theme objects, route names, and other constants benefit from literal type inference.

  5. Avoid any. If you must escape the type system, prefer unknown and narrow with type guards.

  6. Use lang="ts" in SFCs. Always add lang="ts" to your <script setup> blocks:

    <script setup lang="ts">
    // TypeScript is now active in this component
    </script>
    
  7. Export types alongside values. When creating a module, export both the runtime values and their types so consumers can use them in type annotations.

Edit this page
Last Updated: 10/14/26, 10:27 PM
Contributors: Abdul Hamid, Claude Opus 5.5
Prev
Theming
Next
Navigation