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:
| Option | Value | Reason |
|---|---|---|
target | ES2020 | JavaScriptCore on iOS 16+ supports ES2020 features. |
module | ESNext | Vite handles module bundling; keep source as ESM. |
moduleResolution | bundler | Matches Vite's resolution strategy for imports. |
strict | true | Enables all strict type-checking options. |
jsx | preserve | Allows Vue SFC <script setup lang="ts"> to work correctly. |
lib | ["ES2020"] | Matches JavaScriptCore/V8. Do not add DOM — there is no browser. |
skipLibCheck | true | Vue 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.vue | See above | Resolves 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
Enable strict mode. Set
"strict": truein yourtsconfig.json. This catches null errors, implicitanytypes, and other common mistakes.Use
createStyleSheetover inline objects. You get autocomplete, compile-time validation, and cleaner templates.Type your composable return values. Explicitly defining return interfaces makes composables easier to consume and documents their API.
Leverage
as constfor constants. Theme objects, route names, and other constants benefit from literal type inference.Avoid
any. If you must escape the type system, preferunknownand narrow with type guards.Use
lang="ts"in SFCs. Always addlang="ts"to your<script setup>blocks:<script setup lang="ts"> // TypeScript is now active in this component </script>Export types alongside values. When creating a module, export both the runtime values and their types so consumers can use them in type annotations.