Global Properties and Config
app.config.globalProperties reaches templates only; app.provide with a typed key is the better default, and this is why.
app.config.globalProperties.$name = value // template-only; prefer app.provide(key, value)app.config.globalProperties is an object whose keys become readable on every component instance in one app. It exists because Vue 2 had Vue.prototype, and libraries used it to install things like this.$http. Assign app.config.globalProperties.$money = fn and every template in the app can write {{ $money(1250) }}. If a component has its own property of the same name, the component's wins.
Why it is a last resort
It reaches templates and the older Options API, and nothing else. <script setup> code has no component instance to read it from, so a global property is invisible exactly where logic is written. It is untyped until you augment Vue's ComponentCustomProperties interface, and that augmentation replaces Vue's types instead of extending them if the file contains no top-level import or export. It also hides dependencies: a component using $money declares nothing, so anything that mounts it — a test, a second app, a story — has to know to install the plugin first.
The better default
app.provide(key, value) with an InjectionKey gives a typed value that is visible in script, overridable for a subtree, and replaceable in a test. For a value that never varies, a plain module import is simpler again and needs no Vue API at all. Choose app-level provide when the value belongs to one app instance: per request under server rendering, per widget when a page mounts several apps.
The rest of app.config
errorHandler and warnHandler, performance, optionMergeStrategies, throwUnhandledErrorInProduction, idPrefix (added in 3.5; prefixes the ids from useId()) and compilerOptions, which applies only to builds that include the runtime compiler.
The example renders a total as formatted currency, with the formatter injected rather than global.
Example
<script setup lang="ts">
import { inject } from 'vue'
import { formatMoneyKey } from '../keys'
const props = defineProps<{ cents: number }>()
// Typed, readable here in script, and replaceable in a test.
const formatMoney = inject(formatMoneyKey, (cents: number) => `${cents}c`)
</script>
<template>
<p>
Total <strong>{{ formatMoney(props.cents) }}</strong>
</p>
</template>
<style scoped>
strong {
font-variant-numeric: tabular-nums;
}
</style>When to use it
- Making a currency or date formatter available to every template in an app.
- Typing a plugin's $-prefixed property so vue-tsc accepts it in templates.
- Replacing a global property with a provided value so tests can swap the implementation.
- Routing uncaught component errors through app.config.errorHandler.
More examples
Both forms side by side in the entry file
The same function installed twice, once each way, so the difference is concrete. The provided version is what components should use. The global property earns its place in one situation: a large template-heavy codebase, or an Options API codebase being migrated, where rewriting hundreds of templates to inject a formatter is not worth it. Both must be set before mount.
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { formatMoneyKey } from './keys'
const gbp = new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP'
})
const formatMoney = (cents: number) => gbp.format(cents / 100)
// formatMoney(1250) // Output: "£12.50"
const app = createApp(App)
// Preferred: typed, and reachable from <script setup> via inject.
app.provide(formatMoneyKey, formatMoney)
// Last resort: usable as {{ $money(1250) }} in templates, nowhere else.
app.config.globalProperties.$money = formatMoney
app.mount('#app')The typed injection key
Two lines that do the work the global property cannot. A Symbol cannot collide with a key from another plugin, unlike the string 'formatMoney'. The InjectionKey<T> cast carries the type across the provide and inject pair, so inject(formatMoneyKey) is known to return the function type - or that type unioned with undefined when no default is passed. Keep keys in their own module so providers and consumers import the same symbol instead of two different ones.
// src/keys.ts
import type { InjectionKey } from 'vue'
export const formatMoneyKey = Symbol('formatMoney') as InjectionKey<
(cents: number) => string
>Making a global property type-check
Needed whenever you or a plugin set app.config.globalProperties in a TypeScript project. Vue exposes ComponentCustomProperties for exactly this, and the module being augmented is 'vue'. The export {} line is load-bearing: TypeScript module augmentation only applies inside a module, so a file with no top-level import or export overwrites the original interface instead of adding to it, and the error that follows points nowhere near this file. The file also has to be included by tsconfig.json.
// src/globals.d.ts
// Without this file, {{ $money(1250) }} is not checked by vue-tsc.
export {}
declare module 'vue' {
interface ComponentCustomProperties {
$money: (cents: number) => string
}
}
Discussion