TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

In JS, annotating a function expression with a new signature should make it a constructor function.

Open jho1965us opened this issue 3 years ago • 2 comments

Bug Report

🔎 Search Terms

is:issue is:open explicit assign is:issue is:open assign is:issue is:open prototype

🕗 Version & Regression Information

When did you start seeing this bug occur? version 4.7.4

also tested version 4.0.5, 4.8.0-beta and nightly all seems to have same behavior prior to 4.0,5 there seem to other issues as well

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about prototype

⏯ Playground Link

https://www.typescriptlang.org/play?ts=4.7.4&ssl=1&ssc=77&pln=1&pc=4&filetype=js#code/PTAEFEA8AcBsEsDG8AuoUE9oFMAmpoAnAexVK21AFtsqAjbQgZ1AHNs0UALbeQ9CqGIA3RgHdCqFNgB2QuQEMmTeKxk0ZKALAAoEKADqxQgGsWAM3gzK546BnGqC2KHMBXGYhTxiMprt1gACog0AABTBxcbHNQAG84hQAKW2IALlAAMWJiAEoM7OIAGjoUnIKc-KycgF8a6uJQIOBAkPDIvBj4uOsxJKrCooISMg6K4oJxuoaAYV8mFEI3LztmgL02iME4wvq1nWEFflSARlAAXniagG5dU4A6BQvXDy8fRROyvPjQQg43QhyVLXUD1fQAeQA0kMHk94CwvuNchcAHwNUAKGT4BQnADEqVA8IadxyJ3udBBEOhoDohIRqSRqOJOlaoS2OHihTmfkWyzIhD2LQORwaACZnu5PN5fA0AMz9H7ceH3aDPOL1G66QqilUjcg4R4S17SxSiz6pZFxX7-QGuHIg+qgfQAIQAggARIba3WkfXYQ1ExEYmQYZHnNGYjDB7FmumgIORsMRkNanI66CGy6St4yhSi0VfS3WlAAoH20GgJ1gN2esUqgMsZIMhpJ9F0NxoPPiwPNxNMyPrQ78Oixy7eoi+jrkylgKFDCejQREwrRusLv3kuNBwqtldMRpEkcnXRDmn557j6dV0BzghxleY-CX2k98otpl7g8sEei3RAA

💻 Code

/// Explicit typed prototype members get their type overwritten on assignment
// Works fine for normal functions

/** @typedef {{a(foo: Foo): Foo,b(foo: Foo): Foo}} Foo */
/** @typedef {{new(): Foo, prototype: Foo, p: Foo}} FooConstructor */

/** @type {Foo} */
var foo1 = {};
foo1.a = function a1(foo) { return foo; } // OK, foo1.a is (foo: Foo) => Foo and a1#foo is Foo
foo1.b; // OK, b is (foo: Foo) => Foo

/** @type {FooConstructor} */
var Foo2 = function Foo3() { this.p = {} };
Foo2.prototype.a = function a21(foo) { return foo; }  // BAD, Foo2.prototype.a is (foo: any) => any and a21 is (foo: any) => any
Foo2.p.a = function a22(foo) { return foo; }  // BAD, Foo2.p.a is a(foo: Foo) => Foo but a22 is (foo: any) => any

var b21 = Foo2.prototype.b; // OK, prototype is Foo and Foo2.prototype.b is (foo: Foo) => Foo so is b21
var b22 = Foo2.p.b; // OK, p is Foo and Foo2.p.b is (foo: Foo) => Foo so is b22

🙁 Actual behavior

See comment in code the actual type is concluded from tool tip when hovering over the name in question on play ground page

🙂 Expected behavior

expect Foo2.prototype.* and Foo2.p.* should behave similar to foo1.*

jho1965us avatar Sep 05 '22 17:09 jho1965us

@sandersn, can you help parse the multiple issues here?

andrewbranch avatar Sep 09 '22 22:09 andrewbranch

There are 3 distinct issues here, caused by one core missing feature: A @type annotations with a new signature doesn't provide a contextual type that makes a function expression a constructor function. Currently, the compiler focusses so much on inference of constructor functions that it doesn't allow people to provide a type for them upfront.

Given some typescript type annotations:

// instance side
type Foo = {
    a(foo: Foo): Foo,
    b(foo: Foo): Foo
}
// static side
type FooConstructor = {
    new(): Foo,
    p: Foo
}
  1. A @type annotation with a new signature doesn't provide a contextual type to a function expression that makes it a constructor function, so assignments in the body aren't this-property assignment declarations, and they don't contextually type their initialisers:
/**
 * @type {FooConstructor}
 */
var Foo = function() {
    this.a = foo => foo // Expected: foo: Foo, got foo: any
    this.b = foo => foo // same
}
  1. A @type annotation with a new signature doesn't provide a contextual type to what should be prototype-property assignments:
Foo.prototype.a = function (foo) { return foo } // Expected foo: Foo, got foo: any
  1. A @type annotation with a new signature doesn't provide a contextual type to what should be class-property assignments:
Foo.p = { a: foo => foo, b: foo => foo } // Expected foo: foo, got foo: any
// also fails:
Foo.p.a = foo => foo

sandersn avatar Oct 17 '22 22:10 sandersn