Skip to main content
Engineering7 min readMarch 3, 2026

TypeScript in Nuxt: Getting the Type Safety You Actually Want

A practical guide to TypeScript in Nuxt 3 and 4 — typed composables, typed routes, typed API responses, auto-import type augmentation, and the tsconfig that works.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

TypeScript support in Nuxt has come a long way. Early Nuxt 3 had rough edges — auto-imported composables would not be recognized by the type checker, component props from other libraries would not infer correctly, and getting the tsconfig right required trial and error. Most of those problems are solved in Nuxt 4, and the ones that remain have well-established workarounds.

This guide covers building a genuinely type-safe Nuxt application — not just adding TypeScript syntax, but having the type checker actually catch the bugs that matter.

The Right tsconfig.json

Nuxt generates a .nuxt/tsconfig.json that extends from your root config. Your root tsconfig.json should look like this:

{
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true
  }
}

The .nuxt/tsconfig.json sets up the paths for auto-imports and Nuxt-specific type definitions. Extending it means you get those automatically.

Turn on strict mode. It enables a collection of checks that catch real bugs: strictNullChecks, strictFunctionTypes, noImplicitAny. Projects that avoid strict mode to save time end up with types that lie — string | null becomes string, errors get ignored, and the type checker becomes noise rather than signal.

Typed Auto-Imports

Nuxt auto-imports composables and components, which creates a TypeScript challenge: the types need to be available without explicit import statements. Nuxt handles this by generating a .nuxt/imports.d.ts file with declarations for all auto-imported functions.

After running nuxt prepare (which runs on nuxt dev and nuxt build), all auto-imported composables are typed. If you add a new composable and TypeScript does not recognize it immediately, run nuxt prepare manually.

For custom composables, the type is inferred from the implementation:

// composables/useAuth.ts
export function useAuth() {
  const user = useState<User | null>('user', () => null)
  const isAuthenticated = computed(() => user.value !== null)

  return { user: readonly(user), isAuthenticated }
}

// In any component — fully typed without import:
const { user, isAuthenticated } = useAuth()
//     ^--- User | null     ^--- boolean

Typed Router

The typed router in Nuxt 4 generates route types from your pages/ directory. Enable it:

// nuxt.config.ts
experimental: {
  typedPages: true,
}

After running nuxt prepare, you get type-safe navigation:

// Typed navigateTo
navigateTo({ name: 'users-id', params: { id: '123' } })
//                              ^^^^^^ TypeScript knows 'id' is required

// Typed useRoute
const route = useRoute('users-id')
route.params.id  // typed as string
route.params.nonexistent  // TypeScript error

This catches a whole class of routing bugs at compile time. If you rename a page from pages/users/[id].vue to pages/users/[userId].vue, TypeScript will flag every call that still uses the old id param name.

Typed API Calls

Nuxt's useFetch and $fetch accept a generic type parameter:

interface Post {
  id: string
  title: string
  content: string
  publishedAt: string
}

// Typed fetch
const { data: post } = await useFetch<Post>('/api/posts/123')
post.value?.title  // string | undefined (handles null data state)

// Typed $fetch
const posts = await $fetch<Post[]>('/api/posts')
posts[0].title  // string

For more rigorous type safety, validate API responses at runtime with Zod and derive the types from your schemas:

// types/post.ts
import { z } from 'zod'

export const PostSchema = z.object({
  id: z.string(),
  title: z.string(),
  content: z.string(),
  publishedAt: z.string().datetime(),
})

export type Post = z.infer<typeof PostSchema>
// In your composable
const response = await $fetch('/api/posts/123')
const post = PostSchema.parse(response)
// post is typed as Post and validated at runtime

The combination of TypeScript for compile-time safety and Zod for runtime validation means your API types actually match reality — not just what you hoped the API would return.

Typed Component Props

Use TypeScript interfaces for component props rather than the options-based validator syntax:

// Incorrect: runtime validation only, no TypeScript inference
props: {
  user: {
    type: Object as PropType<User>,
    required: true,
  },
}

// Correct: compile-time type checking
interface Props {
  user: User
  onSelect?: (user: User) => void
  size?: 'sm' | 'md' | 'lg'
}

const props = defineProps<Props>()
const emit = defineEmits<{
  select: [user: User]
  close: []
}>()

The generic syntax provides better TypeScript inference and eliminates a lot of ceremony. The downside is no runtime validation — if you need that, keep your Zod schemas and validate in the composable layer rather than in component props.

Typed Pinia Stores

The composable-style store infers types automatically:

// stores/cart.ts
import type { CartItem } from '~/types/cart'

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  function addItem(item: CartItem): void {
    // TypeScript enforces CartItem shape here
  }

  return { items: readonly(items), total, addItem }
})

// In components:
const cart = useCartStore()
cart.items      // readonly CartItem[]
cart.total      // number
cart.addItem    // (item: CartItem) => void

Global Type Augmentation

For types that should be available globally without importing, add them to a .d.ts file:

// global.d.ts
declare global {
  interface Window {
    analytics: AnalyticsInstance
  }
}

// Augment Vue's ComponentCustomProperties for global properties
declare module 'vue' {
  interface ComponentCustomProperties {
    $config: RuntimeConfig
  }
}

// Augment Nitro's H3Event for custom context properties
declare module 'h3' {
  interface H3EventContext {
    userId?: string
    session?: Session
  }
}

The H3 augmentation is particularly useful for middleware that adds properties to the event context — it makes those properties typed throughout your server routes.

Avoiding Common TypeScript Mistakes

Do not use as casts to silence errors. If you write user as User, you are telling TypeScript to trust you. When that trust is wrong, TypeScript provides no protection. Investigate why the type does not match and fix the root cause.

Do not use any. any disables type checking for that value entirely. Use unknown when you genuinely do not know the type — it forces you to narrow the type before using it.

Do not ignore null and undefined. With strictNullChecks enabled, TypeScript catches null reference errors at compile time. Use optional chaining (?.) and nullish coalescing (??) to handle nullable values explicitly.

Handle async errors. TypeScript does not type the errors in catch blocks. They are typed as unknown. Write a type guard:

function isError(e: unknown): e is Error {
  return e instanceof Error
}

try {
  await riskyOperation()
} catch (e) {
  if (isError(e)) {
    console.error(e.message)  // Now typed as string
  }
}

Running Type Checks in CI

Add nuxi typecheck to your CI pipeline:

# .github/workflows/ci.yml
- name: Type check
  run: npm run typecheck

And in package.json:

{
  "scripts": {
    "typecheck": "nuxi typecheck"
  }
}

nuxi typecheck runs Volar's TypeScript compilation with Vue template awareness — it catches type errors in templates that the regular TypeScript compiler misses. Running this in CI ensures TypeScript errors block deployment rather than quietly accumulating in the codebase.

TypeScript in Nuxt is no longer a thing you have to fight. The auto-import type generation works, the typed router is excellent, and the integration with Pinia and Zod gives you end-to-end type safety across the full stack. The investment in a properly typed codebase pays back every time you refactor with confidence.


Working on a Nuxt TypeScript project and hitting type issues you cannot resolve, or want to add type safety to an existing JavaScript codebase? I can help. Book a call: calendly.com/jamesrossjr.


Keep Reading