Viewed   4.5k times

This is similar to #40796374 but that is around types, while I am using interfaces.

Given the code below:

interface Foo {
  name: string;
}

function go() {
  let instance: Foo | null = null;
  let mutator = () => {
   instance = {
     name: 'string'
   };  
  };

  mutator();

  if (instance == null) {
   console.log('Instance is null or undefined');
  } else {
   console.log(instance.name);
  }
}

I have an error saying 'Property 'name' does not exist on type 'never'.

I don't understand how instance could ever be a 'never'. Can anyone shed some light on this?

Thanks in advance.

 Answers

5

Because you are assigning instance to null. The compiler infers that it can never be anything other than null. So it assumes that the else block should never be executed so instance is typed as never in the else block.

Now if you don't declare it as the literal value null, and get it by any other means (ex: let instance: Foo | null = getFoo();), you will see that instance will be null inside the if block and Foo inside the else block.

Never type documentation: https://www.typescriptlang.org/docs/handbook/basic-types.html#never

Edit:

The issue in the updated example is actually an open issue with the compiler. See:

https://github.com/Microsoft/TypeScript/issues/11498 https://github.com/Microsoft/TypeScript/issues/12176

Saturday, October 8, 2022
 
4

TypeScript uses '<>' to surround casts, so the above becomes:

var script = <HTMLScriptElement>document.getElementsByName("script")[0];

However, unfortunately you cannot do:

var script = (<HTMLScriptElement[]>document.getElementsByName(id))[0];

You get the error

Cannot convert 'NodeList' to 'HTMLScriptElement[]'

But you can do :

(<HTMLScriptElement[]><any>document.getElementsByName(id))[0];
Saturday, October 15, 2022
 
2

useRef is generic if you use it with TypeScript, so you can define the referenced element type like const ref = useRef<Type>();

Looking into the type definitions for the inputRef property in MaterialUI it states:

/**
 * Pass a ref to the `input` element.
 */
inputRef?: React.Ref<any>;

So for a fix you can define your refs like:

const accountRef = useRef<any>();

But the ref is passed through the input field inside the component, better type would be:

const accountRef = useRef<HTMLInputElement>();
Thursday, October 13, 2022
 
4

This has become quite a long answer. If you don't have time to read it all there's a TL;DR at the end.


Analysis

Error Message

At first I didn't really understand why TypeScript mentioning VueClass<Vue> and complaining about the template property. However, when I looked at the type definitions for the Component decorator, things became a bit clearer:

vue-class-component/lib/index.d.ts (parts omitted)

declare function Component<V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC;
// ...
declare function Component<VC extends VueClass<Vue>>(target: VC): VC;

What we can see here is that Component has two signatures. The first one to be used like you did, and the second one without options (@Component class Foo).

Apparently the compiler thinks our usage doesn't match the first signature so it must be the second one. Therefore we end up with an error message with VueClass<Vue>.

Note: In the latest version (3.6.3), TypeScript will actually display a better error, stating both overloads and why they don't match.

Better Error Message

The next thing I did was temporarily comment out the second function declaration in main-project/node_modules/vue-class-component and sure enough we get a different error message. The new message spans 63 lines so I figured it wouldn't make sense to include it in this post as a whole.

ERROR in /tmp/main-project/InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts
../InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts
[tsl] ERROR in /tmp/main-project/InitializeProjectGUI__assets/SingletonComponents/SkipProjectInitializationStepPanel/SkipProjectInitializationStepPanel.ts(10,24)
      TS2322: Type '{ SimpleCheckbox: typeof SimpleCheckbox; }' is not assignable to type '{ [key: string]: VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<string, any>> | AsyncComponentPromise<any, any, any, any> | AsyncComponentFactory<...>; }'.
  Property 'SimpleCheckbox' is incompatible with index signature.
    Type 'typeof SimpleCheckbox' is not assignable to type 'VueConstructor<Vue> | FunctionalComponentOptions<any, PropsDefinition<any>> | ComponentOptions<never, any, any, any, any, Record<string, any>> | AsyncComponentPromise<any, any, any, any> | AsyncComponentFactory<...>'.
      Type 'typeof SimpleCheckbox' is not assignable to type 'VueConstructor<Vue>'.
        Types of property 'extend' are incompatible.
          Type '{ <Data, Methods, Computed, PropNames extends string = never>(options?: import("/tmp/dependency/node_modules/vue/types/options").ThisTypedComponentOptionsWithArrayProps<import("/tmp/depende...' is not assignable to type '{ <Data, Methods, Computed, PropNames extends string = never>(options?: import("/tmp/main-project/node_modules/vue/types/options").ThisTypedComponentOptionsWithArrayProps<import("/tmp/main-...'.
            ...
              Type 'import("/tmp/dependency/node_modules/vue/types/vnode").ScopedSlotReturnValue' is not assignable to type 'import("/tmp/main-project/node_modules/vue/types/vnode").ScopedSlotReturnValue'.
                Type 'VNode' is not assignable to type 'ScopedSlotReturnValue'.

As you can see the error message is quite hard to read and doesn't really point to a specific problem. So instead I went ahead and started reducing the complexity of the project in order to understand the issue better.

Minimal Example

I'll spare you the whole process which involved lots of trial and error. Let's jump directly to the result.

Structure

├─ main-project
│  ├─ node_modules // installed packages listed below (without sub-dependencies)
│  │     ts-loader@6.1.0
│  │     typescript@3.6.3
│  │     vue@2.6.10
│  │     vue-property-decorator@8.2.2
│  │     vuex@3.1.1
│  │     webpack@4.40.2
│  │     webpack-cli@3.3.9
│  ├─ SkipProjectInitializationStepPanel.ts
│  ├─ tsconfig.json
│  └─ webpack.config.js
└─ dependency
   ├─ node_modules // installed packages listed below (without sub-dependencies)
   │     vue@2.6.10
   └─ SimpleCheckbox.ts

main-project/tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "strict": true,
    "moduleResolution": "node"
  }
}

main-project/webpack.config.js:

module.exports = {
    entry: './SkipProjectInitializationStepPanel.ts',
    mode: 'development',
    module: {
        rules: [
            {
                test: /.ts$/,
                loader: 'ts-loader'
            }
        ]
    },
    resolve: {
        extensions: ['.ts', '.js']
    }
};

main-project/SkipProjectInitializationStepPanel.ts:

import { Component } from 'vue-property-decorator';
import 'vuex';

import SimpleCheckbox from '../dependency/SimpleCheckbox';

Component({ template: '', components: { SimpleCheckbox } });

dependency/SimpleCheckbox.ts:

import Vue from 'vue';

export default class SimpleCheckbox extends Vue {}

When running this example from main-project with npm run webpack, we get the exact same error as before. Mission accomplished.

What's Happening?

While I was removing parts of the project to get it down to this minimal example, I learned something very interesting.

You might have noticed the import 'vuex' I've added to SkipProjectInitializationStepPanel.ts. In the original project vuex is of course imported from different places (e.g. main-project/Source/ProjectInitializer/Store/Store.ts) but that's not important for reproducing the issue. The crucial part is that main-project imports vuex and dependency doesn't.

To find out why importing vuex causes this issue, we have to look at the type definitions of vuex.

vuex/types/index.d.ts (first few lines)

import _Vue, { WatchOptions } from "vue";

// augment typings of Vue.js
import "./vue";

import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from "./helpers";

Here we are interested in the second import. The comment actually gives us a hint already: augment typings of Vue.js.

vuex/types/vue.d.ts (parts omitted)

import { Store } from "./index";

// ...

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<any>;
  }
}

And here we have the culprit. The vuex types make use of declaration merging to add $store to the Vue interface of vue. It seems that this augmentation only happens to "local" types in the same node_modules as vuex. Because main-project has the augmented Vue and dependency has the original one, the two don't match.

TS Loader

During all my testing I couldn't reproduce the problem with just tsc. Apparently ts-loader does something differently causing this issue. It could have to do with it using Webpack for module resolution or it could be something entirely different. I don't know.

Solution Approaches

I have some ideas how this problem could be solved or worked around. Although these are not necessarily ready-to-use solutions but rather different approaches and ideas.

Add vuex to dependency

As removing vuex from main-project isn't really an option, the only thing we can do to make both Vue interfaces match, is include vuex in dependency as well.

The odd thing here is that I was able to get this fix working in my minimal example but not in the original project. I haven't figured out why that is.

In addition to that, it's not very elegant and you might have to import vuex from every file that you reference from main-project.

Use a shared node_modules

Having a shared node_modules folder means both projects use the same vue so this problem goes away. Depending on your requirements it might be a good solution to organize the two projects in a way that they share the same node_modules folder. You might also want to take a look at tools like Lerna or Yarn workspaces which can help with this.

Consume dependency as a package

You say that dependency is not [an] npm-dependency yet. Maybe it's time to make it one. Similarly to a shared node_modules directory this would result in both projects using the same vue installation which should fix the issue.

Investigate further

As mentioned before, this only happens with ts-loader. Maybe there is something that can be fixed or configured in ts-loader to avoid this problem.

TL;DR

main-project imports vuex while dependency doesn't. vuex augments the Vue interface using declaration merging adding a $store property to it. Now the Vue from one project doesn't match Vue from other causing an error.

Wednesday, December 7, 2022
 
2

You need to provide type for arrays, by default it is "never". Something like this should work.

let onlineDevicesArray:number[] = [];
let offlineDevicesArray:any[] = [];
Tuesday, December 20, 2022
 
stuhpa
 
Only authorized users can answer the search term. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :