0

The following snippet is a valid JavaScript and I want to implement it in TypeScript.

Give this snippet:

class A {
  val: number;
  constructor() {
    this.val = 1;
  }
}

class B {
  val: number;
  constructor() {
    this.val = 2;
  }
}

function autosense(what: string) {
  if (what === 'a') {
    return new A();
  } else {
    return new B();
  }
}

console.log(new autosense('a').val);

I get this error:

 error TS2350: Only a void function can be called with the 'new' keyword.

23 console.log(new autosense('a').val);

I know autosense is a factory and the new keyword is useless. But I WANT to call it with new autosense() - there are no cons on doing this (the function is used as constructor to a temporary object that is not used actually).

How can I fix this typescript issue and make it accept a valid javascript code?

Manuel Spigolon
  • 11,003
  • 5
  • 50
  • 73
  • 2
    "I want to do this thing because there are no cons and because I want to" there's no pros either. Remove the `new` keyword and this behaves as you'd like, right? – Corey Ogburn Jul 20 '23 at 16:39
  • 1
    "*there are no cons on doing this*" - except that it doesn't work with TS, that it is confusing to whoever reads the code, and that it is slow because it unnecessarily constructs an unused object? – Bergi Jul 20 '23 at 17:07
  • TS challange - does ts do the same things js can do? Maybe the underline question is this one. (I removed the `new` kw if you are happy to know it, anyway I want to find a way to do it or I would like a ts guru that tell me that it is impossible) – Manuel Spigolon Jul 20 '23 at 17:29
  • 1
    "underline"? ...EDIT oh, you must mean *underlying* – jcalz Jul 20 '23 at 17:38
  • If that's the actual question then the answer is [intentionally *no*](https://stackoverflow.com/questions/41750390/what-does-all-legal-javascript-is-legal-typescript-mean). If you want to work around it you can always use type assertions or the like, as shown [in this playground link](https://tsplay.dev/m3E91w). So, which one of these is your actual question (you should probably [edit] to make that clear), and what should we do to proceed here? – jcalz Jul 20 '23 at 17:43
  • Yes, I meant "underlying" I think the `asNewable` is the right answer - I will study it, it generates this code `function asNewable(f) { return f; }`. Please post it as answer! lol to those -1 to a clear question, go away if you are not interested to find a way to do it. – Manuel Spigolon Jul 20 '23 at 17:49
  • @ManuelSpigolon You can always use [a type assertion](https://www.typescriptlang.org/play?useDefineForClassFields=true&target=9&ts=4.7.4#code/MYGwhgzhAECC0G8BQ1XQG5hALmgOwFcBbAIwFMAnAbhTWAHs8IAXCg4Z+igCgEpFaaNMwAWASwgA6TCGgBeaAEYaQgL5J1SUJBgAhAUJm5CpSirqMWbDlz4Ghw8VJnzoAJnOp1mgGYE8HGKM0GAEnBBkTGTcAO4iYMy4VmJ4AOb8yEJiPtCx8czycgoA5GDFGYIOFGTMBBR4+GQxcHye0KrQZCAR9g7Q1bX1jc26rZXeSJMMTPQgZJIg9KnceE25oeGRPZDQ-gDWePQxDTsIqzF5CUmsKem48AA+0LqqvNyl5dJYvFRAA) – Bergi Jul 20 '23 at 18:04

2 Answers2

4

TypeScript intentionally does not support this behavior, see microsoft/TypeScript#2310. The TypeScript team takes the position that when writing TypeScript code (as opposed to type-checking JavaScript code) that if you want something to be newable you should be writing a class.

Furthermore, TypeScript doesn't model a constructor that returns something whose type is incompatible with the class, see microsoft/TypeScript#38519.

Yes, it's true that these are possible in JavaScript, but part of the point of TypeScript is to be opinionated about which JavaScript behaviors are desirable to support, and which ones should be avoided. See What does "all legal JavaScript is legal TypeScript" mean? Of course not everyone agrees which set of behaviors fall into each category, but it's clear from the above linked issues that the TypeScript team is not interested in supporting the sort of code you're writing.


If you want to do it anyway, you'll need to work around it somehow. If you're only going to call it a few times, you could use a type assertion at each call. But if you want something more general, you can abstract the type assertion into a helper function.

That is, you write a generic helper function called, sya, asNewable that just returns its input at runtime, but we assert that the return type is a construct signature (as well as the original function type, why not):

function asNewable<A extends any[], R>(
    f: (...a: A) => R): { new(...a: A): R; (...a: A): R } {
    return f as any; 
}

And then

var autosense = asNewable(function (what: string) {
    if (what === 'a') {
        return new A();
    } else {
        return new B();
    }
});
/* var autosense: {
    (what: string): A | B;
    new (what: string): A | B;
} */

Now autosense is both a function and a constructor, according to the type system, and the following is allowed:

console.log(new autosense('a').val); // okay

Please note that since we've worked around the type system's restrictions, any problems you run into as a result are your responsibility. For example:

const oops = asNewable(() => 3)
const whoops = new oops() // compiles okay, but
//  most likely a RUNTIME ERROR, oops is not a constructor

Arrow functions cannot be called with new. There are possible ways to handle that, but that's out of scope for the question as asked. The point is that you need to be careful when circumventing TypeScript this way, that the thing you've asserted is true in the use cases you care about.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
1

You could also do:

class A {
  val: number;
  constructor() {
    this.val = 1;
  }
}

class B {
  val: number;
  constructor() {
    this.val = 2;
  }
}

interface AutoSense {
  // Overload for 'a' argument
  (what: 'a'): A;
  // Overload for 'b' argument
  (what: 'b'): B;
  // Overload for 'a' argument
  new (what: 'a'): A;
  // Overload for 'b' argument
  new (what: 'b'): B;
  // fallback to B if it is not explicitly A or B
  (what: string): B;
  new (what: string): B;
};

const autosense: AutoSense = function (what) {
  if (what === 'a') {
    return new A();
  }
  return new B();
} as AutoSense;

const instanceA = autosense('a');
console.log(instanceA instanceof A); // Output: true
const instanceB = autosense('b');
console.log(instanceB instanceof B); // Output: true
const instanceB2 = autosense('c');
console.log(instanceB2 instanceof B); // Output: true

const newInstanceA = new autosense('a');
console.log(instanceA instanceof A); // Output: true
const newInstanceB = new autosense('b');
console.log(instanceB instanceof B); // Output: true
const newInstanceB2 = new autosense('c');
console.log(instanceB2 instanceof B); // Output: true

We basically define in the interface all the possible combinations and overload the function.

Uzlopak
  • 322
  • 3
  • 8