Skip to content

Wildcard ambient modules declaration override rules  #32422

@IlCallo

Description

@IlCallo

Search Terms

wildcard ambient module override

Suggestion

I could not find written anywhere how wildcard ambient module declaration precedence work in case of overlaps.

In the original pull request "prefix length" was used as the best fit criteria, but I could not locate where that function is located into current code.
Also, prefix length won't help in case of post-fixed *, but maybe that was just naming and it actually meant "longer match".

This feature should be better documented into its handbook page or, if it has been dropped after first implementation, it would be useful to know why so.

The most closely related question I found on StackOverflow doesn't have an answer.

Use Cases

I have a setup with Vue (Quasar actually) + TypeScript + Jest, using SFC.
I have to mount Vue components into Jest tests (which are written into .ts files), but importing SFC won't work (they are not TS files).
Using a shim for all Vue files (the official solution) partially solves this problem, because at least you get typings for the general Vue instance, but you won't get typings for that particular SFC (data, props, computed, etc).

I currently separated the TS script from the SFC to be able to get the typings by importing from the two different files. Now I'm trying to define shims for the component .vue to work by binding its name with a wildcard to it's TS counterpart.

Unluckily, when I import ./demo/QBtn-demo.vue, I still get *.vue shim instead of the specific component one, and nowhere seems to be found how can I force the override.

If I remove Vue shim, it works, but I'm forced to make a personal shim for every component.

I know it's possible by using triple slash references, but that's not the point of this issue.

Current workaround is to import both the .vue SFC and the TS script and then explicitly cast the SFC to the type of the specific instance.

Examples

shims-vue.d.ts

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

component.d.ts

declare module '*/QBtn-demo.vue' { // <= works when general Vue shim isn't present
  import QBtnDemo from 'test/jest/__tests__/demo/QBtn-demo'; // <= this is the TS file
  export default QBtnDemo;
}

QBtn-demo.vue

<script lang="ts" src="./QBtn-demo.ts"></script>

<template>
  <div>
    <p class="textContent">{{ input }}</p>
    <span>{{ counter }}</span>
    <q-btn id="mybutton" @click="increment()"></q-btn>
  </div>
</template>

QBtn-demo.ts

import Vue from 'vue';

export default Vue.extend({
  name: 'QBUTTON',
  data: function(): { counter: number; input: string } {
    return {
      counter: 0,
      input: 'rocket muffin',
    };
  },
  methods: {
    increment(): void {
      this.counter++;
    },
  },
});

app,spec.ts

import { createLocalVue, mount } from '@vue/test-utils';
import { Quasar } from 'quasar';
import { VueConstructor } from 'vue';

import QBtnDemo from './demo/QBtn-demo.vue'; // <= Gets types as 'Vue' instead of 'QBtnDemo'

describe('Mount Quasar', () => {
  const localVue = createLocalVue();
  localVue.use(Quasar);

  const wrapper = mount(QBtnDemo, { localVue });
  const vm = wrapper.vm;

  it('has a created hook', () => {
    expect(typeof vm.increment).toBe('function'); // <= TS error: could not find 'increment'
  });

  it('sets the correct default data', () => {
    expect(typeof vm.counter).toBe('number');  // <= TS error: could not find 'counter'
    const defaultData = vm.$data;
    expect(defaultData.counter).toBe(0);
  });

  it('correctly updates data when button is pressed', () => {
    const button = wrapper.find('button');
    button.trigger('click');
    expect(vm.counter).toBe(1);  // <= TS error: could not find 'counter'
  });
});

app.spec.ts with casting workaround

import { createLocalVue, mount } from '@vue/test-utils';
import { Quasar } from 'quasar';
import { VueConstructor } from 'vue';

import QBtnDemoComponent from './demo/QBtn-demo.vue';
import QBtnDemo from './demo/QBtn-demo';

describe('Mount Quasar', () => {
  const localVue = createLocalVue();
  localVue.use(Quasar);

  const wrapper = mount(QBtnDemoComponent as typeof QBtnDemo, { localVue });
  const vm = wrapper.vm;

  it('has a created hook', () => {
    expect(typeof vm.increment).toBe('function'); // <= Infered correctly
  });

  it('sets the correct default data', () => {
    expect(typeof vm.counter).toBe('number'); // <= Infered correctly
    const defaultData = vm.$data;
    expect(defaultData.counter).toBe(0);
  });

  it('correctly updates data when button is pressed', () => {
    const button = wrapper.find('button');
    button.trigger('click');
    expect(vm.counter).toBe(1); // <= Infered correctly
  });
});

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Metadata

Metadata

Assignees

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions