# TypeScript with Vue

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Reusability
Source: https://www.devtutorials.ai/learn/vue/reusability/typescript-with-vue

> Adding types to Vue code: props, emits and v-model, refs, reactive objects, computed values and template refs, provide/inject with InjectionKey, generic components, typed Pinia stores and route meta, env.d.ts and checking with vue-tsc.

## Why Types?

- **TypeScript** = JavaScript plus **types**: labels that say what kind of value a variable holds.
- The editor checks types while you type. Mistakes show up before the page runs.
- Vue 3 itself is written in TypeScript, so its types are built in.
- In a `.vue` file, write `<script setup lang="ts">` to turn TypeScript on.
- Types disappear when the code is built. The browser only ever runs JavaScript.

**Simple lesson:** types catch mistakes early. They change nothing at run time.

Picked **JavaScript** in the top bar? This lesson is still worth a read, because most job code and libraries use TypeScript. Each box shows the JavaScript version too, so you can compare. If you only write JavaScript, you can skip the exercises' type parts.

### A Real-World Example

- Meera's dish card needs a `name` (text) and a `price` (number).
- With types, passing `price="cheap"` is a red underline in the editor, not a broken bill later.

interface Dish {
    name: string;
    price: number;
    veg?: boolean;
}

const props = defineProps<{ dish: Dish }>();
const emit = defineEmits<{ add: [name: string, price: number] }>();
</script>

<template>
    <p>
      {{ dish.name }} ₹{{ dish.price }} <span v-if="dish.veg">🌱</span>
      <button @click="emit('add', props.dish.name, props.dish.price)">Add</button>
    </p>
</template>
`,
    "App.vue": `<script setup lang="ts">

const total = ref<number>(0);

function onAdd(name: string, price: number): void {
    total.value += price;
    console.log(\`Added \${name}\`);
}
</script>

<template>
    <p>Total: ₹{{ total }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "DishCard.vue": `<script setup>
const props = defineProps({ dish: { type: Object, required: true } });
const emit = defineEmits(["add"]);
</script>

<template>
    <p>
      {{ dish.name }} ₹{{ dish.price }} <span v-if="dish.veg">🌱</span>
      <button @click="emit('add', props.dish.name, props.dish.price)">Add</button>
    </p>
</template>
`,
  }}
/>

- **What happened:** `defineProps<{ dish: Dish }>()` gave the prop a type. `defineEmits` said `add` sends a name and a price.
- Hover over `dish` or `emit` in the editor to see the types.

In the **JS** flavour, the same component uses runtime options such as `{ type: Object, required: true }`. Vue checks those in the browser and warns in the Console. TypeScript checks earlier, in the editor.

### What You Will Learn

1. How to type props, emits and `v-model`.
2. How to type `ref`, `reactive`, `computed` and template refs.
3. How to share typed values with `provide`/`inject`.
4. How to build a generic component.
5. How to type a Pinia store and route `meta`.
6. What `env.d.ts` and `vue-tsc` do.

## Props, Emits and v-model

<div className="tscroll">

| You want | TypeScript way |
|---|---|
| Props | `defineProps<{ name: string; price?: number }>()` |
| Defaults | `const { price = 50 } = defineProps<…>()` |
| Events | `defineEmits<{ add: [name: string] }>()` |
| `v-model` | `const qty = defineModel<number>({ default: 1 })` |
| Slots | `defineSlots<{ default(props: { item: Dish }): any }>()` |

</div>

const qty = defineModel<number>({ required: true });
const { max = 5 } = defineProps<{ max?: number }>();
</script>

<template>
    <button :disabled="qty <= 1" @click="qty--">−</button>
    {{ qty }}
    <button :disabled="qty >= max" @click="qty++">+</button>
</template>
`,
    "App.vue": `<script setup lang="ts">

const plates = ref<number>(2);
</script>

<template>
    <p>Idli plates: <QtyPicker v-model="plates" :max="4" /></p>
    <p>Bill: ₹{{ plates * 40 }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "QtyPicker.vue": `<script setup>
const qty = defineModel({ type: Number, required: true });
const { max = 5 } = defineProps({ max: Number });
</script>

<template>
    <button :disabled="qty <= 1" @click="qty--">−</button>
    {{ qty }}
    <button :disabled="qty >= max" @click="qty++">+</button>
</template>
`,
  }}
/>

## Refs, Reactive, Computed and Template Refs

- `ref<number>(0)` says the ref holds a number. Often TypeScript guesses it from the start value.
- Give the type when the start value is empty: `ref<Dish | null>(null)`, `ref<Dish[]>([])`.
- `reactive<Order>({...})` uses an interface for the whole object.
- `computed<number>(() => …)` types the result.
- `useTemplateRef<HTMLInputElement>("box")` types an element. It is `null` until mounted, so use `?.`.

interface Dish {
    name: string;
    price: number;
}

const dishes = ref<Dish[]>([]);
const chosen = ref<Dish | null>(null);
const order = reactive<{ table: number; note: string }>({ table: 4, note: "" });
const total = computed<number>(() => dishes.value.reduce((sum, d) => sum + d.price, 0));
const box = useTemplateRef<HTMLInputElement>("box");

onMounted(() => {
    dishes.value = [
      { name: "Dosa", price: 80 },
      { name: "Vada", price: 30 },
    ];
    box.value?.focus();
    console.log("focused:", document.activeElement === box.value);
});
</script>

<template>
    <button v-for="d in dishes" :key="d.name" @click="chosen = d">{{ d.name }}</button>
    <p>Chosen: {{ chosen?.name ?? "nothing yet" }}</p>
    <p>Table {{ order.table }}, menu total ₹{{ total }}</p>
    <input ref="box" v-model="order.note" placeholder="Note for the kitchen" />
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Avoid `any`. It switches the checks off. Use `unknown` for data you have not checked yet, such as an API reply, then narrow it.

## Typed provide/inject

- A plain string key gives `inject()` the type `unknown`.
- An **`InjectionKey<T>`** is a symbol that carries the type. Provide and inject with the same key.
- Keep keys in their own file so both sides import the same symbol.

const theme = inject(themeKey);
</script>

<template>
    <h3 :style="{ color: theme?.colour }">Menu ({{ theme?.name }} theme)</h3>
</template>
`,
    "App.vue": `<script setup lang="ts">

const theme = ref<Theme>({ colour: "darkgreen", name: "leaf" });
provide(themeKey, theme);

function swap(): void {
    theme.value = theme.value.name === "leaf" ? { colour: "crimson", name: "chilli" } : { colour: "darkgreen", name: "leaf" };
}
</script>

<template>
    <button @click="swap">Swap theme</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## Generic Components

- A **generic** component works with any item type, like a list that can show dishes or staff.
- Write `<script setup lang="ts" generic="T">`. Then `T` is "whatever type the parent passes".
- The parent's data decides `T`, so the emitted item has the right type too.

defineProps<{ items: T[]; label: (item: T) => string }>();
const emit = defineEmits<{ pick: [item: T] }>();
</script>

<template>
    <ul>
      <li v-for="item in items" :key="item.id">
        <button @click="emit('pick', item)">{{ label(item) }}</button>
      </li>
    </ul>
</template>
`,
    "App.vue": `<script setup lang="ts">

interface Dish { id: number; name: string; price: number }
interface Staff { id: number; name: string; role: string }

const dishes: Dish[] = [{ id: 1, name: "Dosa", price: 80 }, { id: 2, name: "Pongal", price: 60 }];
const staff: Staff[] = [{ id: 1, name: "Ravi", role: "cook" }, { id: 2, name: "Anu", role: "server" }];
const message = ref<string>("");
</script>

<template>
    <PickList :items="dishes" :label="(d) => d.name" @pick="(d) => (message = \`\${d.name} costs ₹\${d.price}\`)" />
    <PickList :items="staff" :label="(s) => s.name" @pick="(s) => (message = \`\${s.name} is a \${s.role}\`)" />
    <p>{{ message }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "PickList.vue": `<script setup>
defineProps({ items: { type: Array, required: true }, label: { type: Function, required: true } });
const emit = defineEmits(["pick"]);
</script>

<template>
    <ul>
      <li v-for="item in items" :key="item.id">
        <button @click="emit('pick', item)">{{ label(item) }}</button>
      </li>
    </ul>
</template>
`,
  }}
/>

- **What happened:** one component served two lists. In the editor, `d` is a `Dish` and `s` is a `Staff`.

## Typed Stores and Routes

- A setup-style Pinia store needs no extra work: its types come from the refs and functions inside.

const cart = useCartStore();
</script>

<template>
    <button @click="cart.add({ dish: 'Upma', price: 50 })">Add Upma</button>
    <p>{{ cart.lines.length }} items, ₹{{ cart.total }}</p>
</template>
`,
  }}
/>

- Route `meta` is untyped by default. Add the fields once in a `.d.ts` file:

```ts
// src/router.d.ts
import "vue-router";

declare module "vue-router" {
  interface RouteMeta {
    requiresAuth?: boolean;
    title?: string;
  }
}
```

- Now `to.meta.requiresAuth` is a `boolean | undefined` in every guard.

## env.d.ts and vue-tsc

- TypeScript does not understand `.vue` files by itself. `create-vue` adds `env.d.ts` with `/// <reference types="vite/client" />`, and the editor's Vue extension (Vue - Official) does the rest.
- `import.meta.env.VITE_API_URL` gets its type from an `ImportMetaEnv` interface in that file.
- **`vue-tsc`** checks every `.vue` and `.ts` file from the terminal. Vite does not check types while building.
- Run it before every build and in CI:

```bash
npx vue-tsc --noEmit        # check types only
npm run build               # create-vue runs: vue-tsc --build && vite build
```

Vite strips types without checking them, so a type error can still reach production unless `vue-tsc` runs. Make it part of `npm run build` or your CI.

**Simple lesson:** type props, state and keys. Let `vue-tsc` check everything before you ship.

## TL;DR

- **`lang="ts"`** = turns TypeScript on in a `.vue` file.
- **`defineProps<T>()`** = types props. Destructure for defaults.
- **`defineEmits<{ e: [args] }>()`** = types events and their arguments.
- **`defineModel<T>()`** = types a `v-model` prop.
- **`ref<T>()` / `reactive<T>()`** = give the type when the start value is empty.
- **`useTemplateRef<HTMLInputElement>()`** = a typed element, `null` until mounted.
- **`InjectionKey<T>`** = a symbol that carries the type for provide/inject.
- **`generic="T"`** = one component that works with any item type.
- **`vue-tsc`** = checks types from the terminal. Vite does not.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Using `any` everywhere | It silences errors | Use real types or `unknown` | Bugs the checker would catch |
| `ref(null)` for an object | TypeScript will guess | `ref<Dish \| null>(null)` | The type is stuck as `null` |
| Reading a template ref in setup | It exists right away | Use it in `onMounted`, with `?.` | `null` errors |
| String keys for provide/inject | Strings are simpler | Use an `InjectionKey` | `inject()` returns `unknown` |
| TypeScript inside template expressions | Templates are TS too | Keep `as` casts in the script | Errors in some setups |
| Relying on `vite build` for types | The build checks types | Run `vue-tsc` | Type errors reach production |

</div>

## Check Yourself

## Exercises

## What to Remember

- Write `<script setup lang="ts">` to use TypeScript in a component.
- Type props, emits and `v-model` with `defineProps`, `defineEmits` and `defineModel`.
- Give `ref<T>()` a type when the start value is empty or `null`.
- Use `InjectionKey` for provide/inject and `generic` for reusable lists.
- Run `vue-tsc` before building. Vite does not check types.

Next: **Vue Router Basics**
