Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function-type props broken in TypeScript #9357

Open
kjleitz opened this issue Jan 22, 2019 · 18 comments · May be fixed by #11223
Open

Function-type props broken in TypeScript #9357

kjleitz opened this issue Jan 22, 2019 · 18 comments · May be fixed by #11223

Comments

@kjleitz
Copy link

kjleitz commented Jan 22, 2019

Version

2.5.22

Reproduction link

https://jsfiddle.net/keegan_openbay/gehkx7pf/10/
https://jsfiddle.net/keegan_openbay/018rs3ae/11/

(More explanation in the fiddle, but keep in mind that JSFiddle doesn't show TS errors)

Steps to reproduce

  1. Declare a prop of type Function, and with a default function that returns some value; e.g.,
// ...
  props: {
    fooFn: {
      type: Function,
      default: () => true,
    },
  },
// ...
  1. Try to use that function elsewhere in your component options; e.g.,
// ...
  methods: {
    useFooFn(): void {
      const bar = this.fooFn();
      // ...
    },
  },
// ...

What is expected?

type FooFn = typeof this.fooFn; // Function
this.fooFn(); // no errors

What is actually happening?

type FooFn = typeof this.fooFn; // boolean | Function
this.fooFn();
// Cannot invoke an expression whose type lacks a call signature.
// Type 'boolean | Function' has no compatible call signatures.

Vue version: 2.5.22
TypeScript version: 3.0.3

tsconfig.json:

{
  "compilerOptions": {
    "declaration": false,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": ["es7", "dom"],
    "module": "es2015",
    "moduleResolution": "node",
    "sourceMap": true,
    "target": "es5",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "baseUrl": "./app/javascript",
    "noImplicitThis": true
  },
  "include": [
    "app/javascript/**/*.ts",
    "app/javascript/**/*.tsx",
    "app/javascript/**/*.vue"
  ],
  "exclude": [
    "**/*.spec.ts",
    "node_modules"
  ],
  "compileOnSave": false
}
@posva
Copy link
Member

posva commented Jan 23, 2019

I tried adding the test case to the project and couldn't reproduce:

Vue.extend({
  props: {
    isValid: {
      type: Function,
      default: () => true,
    }
  },
  methods: {
    useFooFn(): void {
      const bar = this.isValid()
      alert(bar)
    }
  }
});

@kjleitz
Copy link
Author

kjleitz commented Jan 23, 2019

@posva Are you using the same TypeScript setup and seeing no compile errors?

@posva
Copy link
Member

posva commented Jan 23, 2019

no, I'm using the one we have in the repo

@kjleitz
Copy link
Author

kjleitz commented Jan 23, 2019

Can you try with the setup I posted?

@kjleitz
Copy link
Author

kjleitz commented Jan 23, 2019

@posva

screen shot 2019-01-23 at 3 45 55 pm

screen shot 2019-01-23 at 3 45 12 pm

@kjleitz
Copy link
Author

kjleitz commented Jan 25, 2019

Found a clue:

I just downgraded vue (and vue-template-compiler) to 2.5.17 and it works fine, as it used to. Then I upgraded both to 2.5.18, and now I see a bunch of compiler errors (including this one) which had never occurred before:

// for Function-type props with a default like `() => false`, `(arg: number) => false`, etc.:
// => TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'boolean | Function' has no compatible call signatures.
// (same error occurs with any return value)

function foo(barEl: HTMLElement) { /* ... */ }
foo(this.$el);
// => TS2345: Argument of type 'Element' is not assignable to parameter of type 'HTMLElement'.

const bar = this.$el.innerText;
// => TS2339: Property 'innerText' does not exist on type 'Element'.

Seems that:

  1. Function props' types are being recognized as either Function or the return type of their default function
  2. The type of this.$el has become Element instead of HTMLElement
  3. This occurs in vue/vue-template-compiler 2.5.18+, but not 2.5.17

It may have something to do with "noImplicitThis": true in tsconfig.json; setting it to false removes all those errors (there is one new error in our project after setting it to false but it's to do with a lack of type inference on this.$store, all in one component rather than scattered across the project)

@kjleitz
Copy link
Author

kjleitz commented Jan 25, 2019

Reverting #8537, specifically this change:

diff --git a/types/options.d.ts b/types/options.d.ts
index cc58affe6a..25eb8a0fdf 100644
--- a/types/options.d.ts
+++ b/types/options.d.ts
@@ -133,7 +133,7 @@ export type PropValidator<T> = PropOptions<T> | Prop<T> | Prop<T>[];
 export interface PropOptions<T=any> {
   type?: Prop<T> | Prop<T>[];
   required?: boolean;
-  default?: T | null | undefined | (() => object);
+  default?: T | null | undefined | (() => T | null | undefined);
   validator?(value: T): boolean;
 }

...fixes the Function-type prop issue.

@kjleitz
Copy link
Author

kjleitz commented Jan 25, 2019

And reverting #8809, specifically this change:

diff --git a/types/vue.d.ts b/types/vue.d.ts
index 44a892ead3..3832f2c9e4 100644
--- a/types/vue.d.ts
+++ b/types/vue.d.ts
@@ -21,7 +21,7 @@ export interface CreateElement {
 }
 
 export interface Vue {
-  readonly $el: HTMLElement;
+  readonly $el: Element;
   readonly $options: ComponentOptions<Vue>;
   readonly $parent: Vue;
   readonly $root: Vue;

...fixes the this.$el defaulting to Element issue.

@kjleitz
Copy link
Author

kjleitz commented Jan 25, 2019

I'll see if I can write up a PR for a fix without resurfacing the original issues those PRs were trying to solve.

@kjleitz
Copy link
Author

kjleitz commented Jan 28, 2019

Aaaaand I've realized over the past few days that I am not good enough with TypeScript to figure out how to do this.

Where...

  1. "noImplicitThis": true in tsconfig.json, and
  2. Vue version is 2.5.18 and above,

...in order to get a prop definition such as this:

// ...
  props: {
    isValid: {
      type: Function,
      default: () => true,
    }
  },
// ...

...to yield a type of:

this.isValid //=> Type: () => boolean

...instead of:

this.isValid //=> Type: boolean | () => boolean

You'd have to edit the PropOptions interface such that:

export interface PropOptions<T=any> {
  type?: Prop<T> | Prop<T>[];
  required?: boolean;
  // default?: T | null | undefined | (() => T | null | undefined);
  // I guess...?
  default?: Function extends T ? (T | null | undefined) : (T | null | undefined | (() => T | null | undefined));
  validator?(value: T): boolean;
}

Unfortunately, that example doesn't work, and the types of other properties on Vue are lost. I've tried a lot of different things over the past few days, but clearly I don't have the expertise to understand exactly how to fix the issue.

I believe, currently, a Function-type prop is a unique case. It is (correct me if I'm wrong), the only prop type that does not have the option of a default "factory" function:

props: {
  fnProp1: {
    type: Function,
    default: () => false, // type of this.fnProp1 should be `() => boolean`
  },
  fnProp2: {
    type: Function,
    default: () => (() => false), // type of this.fnProp2 should be `() => (() => boolean)`
  },
  boolProp1: {
    type: Boolean,
    default: false, // type of this.boolProp1 should be `boolean`
  },
  boolProp2: {
    type: Boolean,
    default: () => false, // type of this.boolProp2 should STILL be `boolean`
  },
  strProp1: {
    type: String,
    default: 'hi', // type of this.strProp1 should be `string`
  },
  strProp2: {
    type: String,
    default: () => 'hi', // type of this.strProp2 should STILL be `string`
  },
  // etc.
},

@kjleitz
Copy link
Author

kjleitz commented Jan 28, 2019

Furthermore, if you want to return an object from the default, it completely skips the function type altogether:

// ...

  props: {
    returnsAnObject: {
      type: Function,
      default: () => ({}),
    }
  },

// ...

  // Type SHOULD be `Function`, or `() => {}`, but...
  this.returnsAnObject; //=> Type: {}
  // ...which is not even the [broken] union `{} | () => {}` type like the other cases

  this.returnsAnObject();
  // Cannot invoke an expression whose type lacks a call signature.
  // Type '{}' has no compatible call signatures.

// ...

@kjleitz
Copy link
Author

kjleitz commented Jan 28, 2019

I'd rather not keep bumping this unnecessarily, since it's mostly an echo chamber at the moment, but this 2.5.17 to 2.5.18+ patch update breaks our build, necessitates a lot of boilerplate around what used to be correctly-inferred properties on our components, and the causal changes seem to be fairly clear. Any attention or help would be greatly appreciated!

@pikax
Copy link
Member

pikax commented Apr 23, 2019

if you annotate with the PropType<> it should work, this was a fix on #9733

const Example = Vue.extend({
	template: `
  	<button @click="doSomethingWithFoo()">
    	<slot></slot>
    </button>
  `,

  props: {
    // original issue
    fooFn: {
      type: Function as PropType<()=>string>,
      default: () => { return 'hey this is the default return value'; },
    },


     returnsAnObject: {
      type: Function as PropType<()=>object>,
      default: () => ({}),
    }
  },

  methods: {
    doSomethingWithFoo(): void {
      const obj = this.returnsAnObject(); //obj is object
    	const bar = this.fooFn(); // bar is string
  
      alert(bar);
    },
  },
});

there's an PR vuejs/v2.vuejs.org#2068 to update docs

@romansp
Copy link

romansp commented Sep 12, 2019

Is this issue back on TypeScript 3.6? The following compiles fine on TS 3.5.3 and fails on the latest TS 3.6.3.

Vue: 2.6.10
TypeScript: 3.6.3

import Vue from 'vue';

export default Vue.extend({
  props: {
    cb: {
      type: Function,
      default: () => {},
    },
  },

  created() {
    this.cb();
  }
});
13:10 This expression is not callable.
  No constituent of type 'void | Function' is callable.
    11 | 
    12 |   created() {
  > 13 |     this.cb();
       |          ^
    14 |   }
    15 | });

Removing default: () => {} from cb as well as annotating it with PropOptions<() => void> helps, but this wasn't needed before.

Put a repro here: https://github.com/romansp/vue-typescript-prop-function-default.

May be related to #10455.

@kjleitz
Copy link
Author

kjleitz commented Sep 12, 2019

@romansp I'm fairly confident that hasn't worked without annotation since Vue 2.5.17. Annotating with PropType<...> (note: not PropOptions<...>) works just fine on Vue 2.6.10 and TypeScript 3.6.3, though.

@romansp
Copy link

romansp commented Sep 13, 2019

@kjleitz I'm sure that it does work on TS 3.5.3 and Vue 2.6.10. You can try cloning my repro https://github.com/romansp/vue-typescript-prop-function-default. I just pushed ts-3.5.3 branch where vue serve runs fine.

@kjleitz
Copy link
Author

kjleitz commented Sep 16, 2019

@romansp Ah, I see, you're not using the same tsconfig.json as in my original example. The fact that you're using "strict": true instead of "noImplicitThis": true fixes it in your ts-3.5.3 branch (we've also switched to using "strict": true since the time this ticket was written; much better!). Even with "strict": true though, if you set Vue back to v2.6.8 it's actually still broken in your ts-3.5.3 branch. It's always been wonky.

Vue v2.6.10 & TS v3.5.3 must be one of those special combinations that don't error for function props 🤷‍♂ But even in that branch, the "working" case loses type info from this.cb—better to use type: Function as PropType<() => void>, instead of a bare type: Function,.

kjleitz added a commit to kjleitz/vue that referenced this issue Mar 19, 2020
@kjleitz kjleitz linked a pull request Mar 19, 2020 that will close this issue
13 tasks
@kjleitz
Copy link
Author

kjleitz commented Mar 20, 2020

This is still broken, even with "strict": true; can't use a default for a function-type prop. A more complete example:

const ComponentWithFunctionProps = Vue.extend({
  props: {
    functionProp: {
      type: Function,
      default: () => true,
    },
    functionPropWithBooleanReturnType: {
      type: Function as PropType<() => boolean>,
      default: () => true,
    },
    booleanProp: {
      type: Boolean,
      default: true,
    },
    booleanPropWithFunctionDefault: {
      type: Boolean,
      default: () => true,
    },
  },
  methods: {
    test(): void {
      // ERROR!
      // (property) functionProp: boolean | Function
      // -------------------------------------------
      // This expression is not callable.
      //   No constituent of type 'boolean | Function' is callable.ts(2349)
      this.functionProp();

      // ERROR!
      // (property) functionPropWithBooleanReturnType: boolean | (() => boolean)
      // -----------------------------------------------------------------------
      // This expression is not callable.
      //   Not all constituents of type 'boolean | (() => boolean)' are callable.
      //     Type 'false' has no call signatures.ts(2349)
      this.functionPropWithBooleanReturnType();

      // const foo: boolean
      const foo = this.booleanProp;

      // const bar: boolean
      const bar = this.booleanPropWithFunctionDefault;
    },
  },
});

I submitted a fix for this in #11223.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants