In a Vue 3 TypeScript project, how do you type a $t property that a plugin adds through app.config.globalProperties?
answer
- module augmentation
- declare module vue
- an interface for instance properties
- file must be a module
- include it in tsconfig
basics
~20 sAugment Vue's ComponentCustomProperties interface with declare module 'vue' in a file that is itself a module (has a top-level import or export), and make sure tsconfig includes it; templates and this then know $t's type.
solid answer
~40 s`globalProperties` is assigned at runtime, so TypeScript cannot see it on its own. Vue exposes an empty `ComponentCustomProperties` interface for this: you add `declare module 'vue' { interface ComponentCustomProperties { $t: (key: string) => string } }`, and declaration merging adds `$t` to every component instance type, which `vue-tsc` also uses for templates. The file must be a TypeScript **module**, so it needs at least one top-level `import` or `export` (even `export {}`); otherwise the declaration replaces Vue's types instead of augmenting them. It must be included by `tsconfig.json`, and a published plugin points `types` in its `package.json` at it. Typing the plugin itself as `Plugin<[I18nOptions]>` also makes `app.use` check the options you pass.
code
ts · 8 lines// src/i18n.d.ts (included by tsconfig.json)
import type { I18n } from './plugins/i18n'
declare module 'vue' {
interface ComponentCustomProperties {
$t: I18n['t']
}
}go deeper
Recall that runtime globalProperties need a matching type declaration, and that it goes on ComponentCustomProperties in the vue module.
Explain module augmentation, the must-be-a-module rule with export {}, tsconfig inclusion and typing plugin options with Plugin<[Options]>.
Show how a published plugin ships its augmentation via package.json types, and why the type claim must be kept in sync with what install actually assigns.
Argue for keeping global instance properties rare and typed in one place, and preferring typed injection keys for logic-side APIs across a large codebase.
## Why TypeScript cannot see $t on its own A plugin adds `$t` with a runtime assignment: `app.config.globalProperties.$t = t`. The component instance types that TypeScript and the template type checker use are built from each component's own props, setup bindings and options. Nothing in that inference looks at what some plugin assigned at runtime, so without help `$t` is an unknown property, and templates using it report an error under `vue-tsc`. ## The fix: augment ComponentCustomProperties Vue exports an intentionally empty interface, `ComponentCustomProperties`, and mixes it into the public instance type of every component. TypeScript's **declaration merging** lets any file add members to an interface declared elsewhere, and **module augmentation** is the form of it that targets a module's exports: ```ts export {} declare module 'vue' { interface ComponentCustomProperties { $t: (key: string) => string } } ``` After this, `this.$t` in Options API code and `$t(...)` in any template type-check, with the declared signature. ## Placement rules 1. **The file must be a module.** A file with no top-level `import` or `export` is a global script. There, `declare module 'vue'` is treated as declaring the module from scratch, which overwrites Vue's real types instead of extending them. One `export {}` line is enough to fix it. 2. **The compiler must see it.** Put it in a `.ts` or `.d.ts` file covered by `tsconfig.json`'s `include`. 3. **Ship it with the library.** A plugin published to npm points the `types` field of its `package.json` at the declaration file that contains the augmentation, so consumers get it just by importing the plugin. 4. **Keep the type honest.** The augmentation is only a type claim. If `install` assigns a different signature, or is never installed, the compiler still believes the claim. ## Related interfaces | Interface in `vue` | What augmenting it types | |---|---| | `ComponentCustomProperties` | properties on every component instance, such as `$t` | | `GlobalComponents` | components registered app-wide with `app.component` | | `GlobalDirectives` | directives registered app-wide with `app.directive` | | `ComponentCustomProps` | extra props every component accepts | A plugin that registers components or directives usually augments the matching interface as well, so templates know those tags and directives too. ## Typing the plugin and its options The `Plugin` type takes the install arguments as a tuple, so `Plugin<[I18nOptions]>` types `install(app, options: I18nOptions)`. `app.use(i18nPlugin, opts)` then checks `opts` against `I18nOptions`, and omitting required options is a type error. A plain object form, `Plugin<I18nOptions>`, is also accepted and behaves the same way for a single options argument. For optional options, use a labelled optional tuple such as `Plugin<[options?: I18nOptions]>`. ## Global properties versus provide, for types The augmentation types instance access, which `<script setup>` code does not use, because it has no `this`. For logic-side code the provide channel is typed separately, through an `InjectionKey<T>` symbol, with no augmentation at all. That is one more reason plugins commonly expose both channels: the global property for templates, the typed injection for composition code. ## A quick way to verify After adding the augmentation, three checks confirm it is wired correctly: - hover `$t` inside a template in an editor running the Vue language tools: it should show the declared signature rather than an error; - run `vue-tsc --noEmit`: a template calling `$t(42)` should now fail, proving the type is enforced, not merely tolerated; - open any file that imports `ref` from `vue` and confirm it still resolves; if it does not, the augmentation file is not a module and has replaced Vue's types. In a library, also check that the declaration is reachable from the consumer's side: an augmentation that exists only in the source folder, and not in the published types, types nothing for the people installing the plugin. ## Common mistakes - Writing the augmentation in a file without any `import` or `export`, then wondering why every Vue import broke. - Augmenting `ComponentCustomProps` (props) when the goal was an instance property. - Forgetting the `$` prefix convention and colliding with a component's own property name. - Casting at every call site (`(this as any).$t`) instead of augmenting once.
- Why would a declaration file with only declare module 'vue' break all Vue imports?Without a top-level `import` or `export` the file is a global script, and `declare module 'vue'` there declares the module anew instead of augmenting it. TypeScript then sees only your interface as the contents of `vue`, so `ref`, `createApp` and the rest disappear from its view. Adding `export {}` turns it back into an augmentation.
- How does a plugin make its globally registered components type-check in templates?It augments `GlobalComponents` in the same way, mapping each registered name to the component's type, for example `interface GlobalComponents { UiButton: typeof UiButton }`. The template type checker then knows the tag and its props.
saying these in an interview costs you the question
- Assigning to globalProperties is enough for TypeScript to see the property
- The augmentation works in any .d.ts file, with or without imports
- ComponentCustomProps is the interface for instance properties like $t
- The augmentation also types this.$t inside script setup code
- Type augmentation registers the property at runtime too