Extending TypeScript Type Definitions
Due to the dynamic nature of JavaScript, some libraries encourage you to extend their default types. An example of that is Vue.js. Its plugins often extend the Vue
prototype that's available in every component. How can this best be handled with TypeScript?
The easiest way to learn is to look at the plugins that already do this. The official Vue Router is such a plugin with full TypeScript support. It extends the Vue
prototype with $route
and $router
members to make them available inside the components:
import Vue from "vue";
import Component from "vue-class-component";
@Component({})
export default class extends Vue {
goBack() {
window.history.length > 0 ? this.$router.go(-1) : this.$router.push("/");
}
}
The prototype is still imported from the vue
package but it includes full type information about the members added by the Vue Router plugin. How is this possible? The answer is in the types/vue.d.ts
file in the vue-router
package:
declare module "vue/types/vue" {
interface Vue {
$router: VueRouter;
$route: Route;
}
}
The same pattern can be used for your plugins. And even for third-party plugins without TypeScript to make them more convenient to use with TypeScript.
The Elastic APM Vue package is an example of such a plugin. The documentation instructs you to install it in the main.ts
file:
import router from "./router";
import { ApmVuePlugin } from "@elastic/apm-rum-vue";
Vue.config.productionTip = false;
Vue.use(ApmVuePlugin, {
router,
config: {
serviceName: "app-name",
},
});
However, the TypeScript compiler will throw the following error:
Could not find a declaration file for module
@elastic/apm-rum-vue
.node_modules/@elastic/apm-rum-vue/dist/lib/index.js
implicitly has anany
type.Try
npm install @types/elastic__apm-rum-vue
if it exists or add a new declaration (.d.ts
) file containingdeclare module '@elastic/apm-rum-vue';
Since there's no @types/elastic__apm-rum-vue
package available, a local type declaration must be created to resolve the issue. I decided to put it in types/elastic__apm-rum-vue/index.d.ts
:
declare module "@elastic/apm-rum-vue";
I also had to modify the tsconfig.json
file to tell the compiler about this new type declaration:
{
"compilerOptions": {
// ...
"types": ["webpack-env", "types/elastic__apm-rum-vue"]
// ...
},
"include": [
"types/**/*.d.ts"
// ...
]
// ...
}
Only now, the issue with extending the Vue
prototype becomes apparent:
import Vue from "vue";
import Component from "vue-class-component";
@Component({
created(this: About) {
this.span = this.$apm.startSpan("create-mount-duration", "custom");
},
mounted(this: About) {
this.span?.end();
},
})
export default class About extends Vue {
span?: Span;
}
The compiler knows nothing about the $apm
member that was added by the plugin (nor about the Span
type for that matter):
Property
$apm
does not exist on typeAbout
.Cannot find name
Span
.
The issue can be resolved by following the approach from the Vue Router plugin:
import { ApmBase } from "@elastic/apm-rum";
declare module "vue/types/vue" {
interface Vue {
readonly $apm: ApmBase;
}
}
I put the code above in src/vue.d.ts
. I also took advantage of the fact that the core Elastic APM package comes with TypeScript support. This means that instead of only fixing the build problem I also got full type definitions for the new $apm
prototype member (including the Span
type).
A working sample project featuring this approach is available from my GitHub repository.
A TypeScript type definition can be extended in a different type definition file by declaring a module matching the original location of the type definition (vue/types/vue
matches the vue.d.ts
file in the types
folder of the vue
NPM package). Any members declared in a type will be added to the members declared in the original type definition.