Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Type-Safe Global Properties and Methods in Vue 2 with TypeScript

Tech 1

Prefer a plugin with module augmentation

Attaching fields directly to Vue.prototype works at runtime, but TypeScript will not know those members exist unless you augmant Vue’s types. The recommended approach is to provide a plugin and a .d.ts declaration.

src/plugins/globals.ts

import Vue, { PluginObject } from 'vue';

function sayHello(this: Vue, msg: string = 'hello'): void {
  // example runtime logic
  // eslint-disable-next-line no-console
  console.log(`[hello]: ${msg}`);
}

const GlobalsPlugin: PluginObject<void> = {
  install(VueCtor) {
    // attach properties/methods on the prototype
    Object.defineProperty(VueCtor.prototype, '$hello', {
      value: sayHello,
      writable: false,
    });

    Object.defineProperty(VueCtor.prototype, '$token', {
      value: 'sample-token',
      writable: true,
    });
  },
};

export default GlobalsPlugin;

main.ts

import Vue from 'vue';
import GlobalsPlugin from '@/plugins/globals';

Vue.use(GlobalsPlugin);

new Vue({ el: '#app', render: h => h(App) });

src/types/vue-globals.d.ts

// ensure this file is included by tsconfig.json (via include or files)

declare module 'vue/types/vue' {
  interface Vue {
    $hello(message?: string): void;
    $token: string;
  }
}

Usage in components is now fully typed:

this.$hello('boot');
this.$token = 'new-token';

Notes

  • Importing Vue in the .d.ts is not necessary for ambient module augmentation as long as the declaration file is part of the TypeScript program (included by tsconfig). Some setups do import type Vue to force module resolution, but Vue CLI projects usually pick it up automatically becuase all .d.ts files under src are included.
  • TypeScript finds the declaration because .d.ts files are part of the compilation and declare module 'vue/types/vue' merges with Vue’s global typings.

Alternative: global mixin

A global mixin can inject data and methods into every component instence. Avoid prefixing data keys with $ because Vue reserves that prefix and will not proxy them onto the instance.

main.ts

import Vue, { ComponentOptions } from 'vue';

const globalsMixin: ComponentOptions<Vue> = {
  data() {
    return {
      sharedData: 'from mixin',
    };
  },
  methods: {
    mixinHello(this: Vue) {
      // eslint-disable-next-line no-console
      console.log('mixin hello');
    },
  },
};

Vue.mixin(globalsMixin);

Type declarations for mixin-added meembers (optional but recommended):

src/types/vue-mixin.d.ts

declare module 'vue/types/vue' {
  interface Vue {
    sharedData: string;
    mixinHello(): void;
  }
}

Correct usage:

this.mixinHello();
console.log(this.sharedData);

Why this.$testData was undefined in data

  • Vue does not proxy properties starting with $ or _ from data onto the instance to avoid colliding with internal APIs. They remain under this.$data. Use a non-$ name (e.g., sharedData) for reactive data.

Pluggable global event bus (typed)

A small, typed event hub can be installed as a plugin and exposed as this.$eventBus.

src/libs/event-bus.ts

export type Handler<T = any> = (payload: T) => void;

class EventHub {
  private registry = new Map<string, Set<Handler>>();

  private static _instance: EventHub | null = null;

  static get instance(): EventHub {
    if (!this._instance) this._instance = new EventHub();
    return this._instance;
  }

  on<T = any>(event: string, fn: Handler<T>): void {
    if (!this.registry.has(event)) this.registry.set(event, new Set());
    this.registry.get(event)!.add(fn as Handler);
  }

  once<T = any>(event: string, fn: Handler<T>): void {
    const wrapper: Handler<T> = (payload) => {
      this.off(event, wrapper);
      fn(payload);
    };
    this.on(event, wrapper);
  }

  emit<T = any>(event: string, payload: T): void {
    const fns = this.registry.get(event);
    if (!fns || fns.size === 0) {
      // eslint-disable-next-line no-console
      console.warn(`No listeners for event: ${event}`);
      return;
    }
    for (const fn of Array.from(fns)) fn(payload);
  }

  off(event: string, fn?: Handler): void {
    const set = this.registry.get(event);
    if (!set) return;
    if (!fn) {
      this.registry.delete(event);
      return;
    }
    set.delete(fn);
    if (set.size === 0) this.registry.delete(event);
  }
}

export default EventHub;

src/plugins/event-bus.ts

import Vue, { PluginObject } from 'vue';
import EventHub from '@/libs/event-bus';

const EventBusPlugin: PluginObject<void> = {
  install(VueCtor) {
    Object.defineProperty(VueCtor.prototype, '$eventBus', {
      value: EventHub.instance,
      writable: false,
    });
  },
};

export default EventBusPlugin;

src/types/vue-event-bus.d.ts

import type EventHub from '@/libs/event-bus';

declare module 'vue/types/vue' {
  interface Vue {
    $eventBus: EventHub;
  }
}

main.ts

import Vue from 'vue';
import EventBusPlugin from '@/plugins/event-bus';

Vue.use(EventBusPlugin);

Usage

const handler = (msg: string) => console.log('event payload:', msg);

this.$eventBus.on('ready', handler);
this.$eventBus.emit('ready', 'booted');
this.$eventBus.off('ready', handler);
// remove all listeners for an event
this.$eventBus.off('ready');

Additional tips

  • Keep all augmentations in dedicated .d.ts files under src/types and ensure tsconfig.json includes that directory.
  • Avoid exporting anything from augmentation files; exporting turns them into modules and prevents ambient merging.
  • For Vue 3, use app.config.globalProperties and augment ComponentCustomProperties instead of vue/types/vue.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.