1

How do I represent an arbitrary type of value in Typescript? For example:

class Checker {
  /**
   * @param first a value
   * @param second the type of a value
   * @return true if "first" is an instance of type "second"
   */
  public instanceOf(first: unknown, second: Class): boolean {
    return first instanceof second;
  }
}

The above code has to behave the same as first instanceof second but I can't figure out what type second must have to make this work.

This question is related to Is there a type for "Class" in Typescript? And does "any" include it? but none of the answers work for classes with private constructors (e.g. LocalDate). I cannot simply change the constructor to public because users must be able to pass in values from third-party libraries.

What doesn't work

  • new (...args: never[]) => unknown) fails with:

Cannot assign a 'private' constructor type to a 'public' constructor type.

  • public instanceOf<T>(first: unknown, second: T) does not work because it allows users to pass non-type values into second such as 1234 or "test". Remember, if first instanceof 1234 results in a compiler error then so should instanceOf(first, 1234).

  • public instanceOf<T extends object>(first: unknown, second: T) blocks non-type values but then the method implementation fails to compile with:

TS2359: The right-hand side of an 'instanceof' expression must be of type 'any' or of a type assignable to the 'Function' interface type.

Gili
  • 86,244
  • 97
  • 390
  • 689
  • 1
    Can you just use generics? Like this: `public instanceOf(first: unknown, second: T): boolean;` – Bunyamin Coskuner Oct 27 '20 at 06:30
  • 1
    `new (...args: never[]) => unknown)` is the way to represent a class. The whole point of a provate constructor is you can't have anyone else invoke it, so you will not be able to get a private constructor into a constructor reference. Consider making it public – Titian Cernicova-Dragomir Oct 27 '20 at 07:54
  • @TitianCernicova-Dragomir Users need to be able to pass in arbitrary types (defined by third-party libraries) so I cannot simply change the constructor to public. – Gili Oct 28 '20 at 01:42
  • @TitianCernicova-Dragomir using generics seems to break the implementation. See the updated question. – Gili Oct 28 '20 at 03:45

2 Answers2

0

As far as I can tell, Function is the only way to go:

class Checker {
  /**
   * @param first a value
   * @param second the type of a value
   * @return true if "first" is an instance of type "second"
   */
  public instanceOf(first: unknown, second: Function): boolean {
    return first instanceof second;
  }
}

eslint warns:

Avoid the Function type, as it provides little safety for the following reasons:

  • It provides no type safety when calling the value, which means it's easy to provide the wrong arguments.
  • It accepts class declarations, which will fail when called, as they are called without the new keyword.

Can we do better?

Gili
  • 86,244
  • 97
  • 390
  • 689
  • "*Can we do better?*" better how? What's wrong with `Function`? You *can* still have objects created from a constructor function rather than a `class` construct. – VLAZ Oct 28 '20 at 05:42
  • @VLAZ I mean, can we use another construct that does not have the problems mentioned by eslint's documentation? The second warning doesn't seem to apply in my case (since I'm only invoking `instanceof`) but what about the first one? – Gili Oct 29 '20 at 05:03
  • Neither the first, nor the second item is relevant here. You want to take a constructor (either class or function one) and *not* call it. Therefore, you won't provide any arguments. Since you're not calling it, `new` is also irrelevant. In fact (again) you want both a class and a constructor function to be passed in. There is no real distinction between the two here - in both cases it's just a type of function. You could make `second` as `(...args: any[]) => void` which will also accept any function. It's superfluous but will probably avoid the linter error. – VLAZ Oct 29 '20 at 05:59
  • @VLAZ It sounds like you're just providing the shape of an arbitrary function. Is there an advantage to using this over `Function`? The latter is more readable. – Gili Oct 30 '20 at 13:40
  • 1
    There is no advantage over `Function`. Well, other than avoiding the linter error but that's it. I'd even say that the linter is wrong *in this case* because `Function` is unsafe *in general* but in this case it's actually the correct type - you want to accept any function. You're not going to call it, so the signature or being a class or not is irrelevant. All of this is nicely handled by `Function`, hence my initial question of what's wrong with it. If the lint error is the only thing that's a problem, I'd advise suppressing it for this code rather than changing the code. – VLAZ Oct 30 '20 at 13:53
0

I think that the Function type is probably correct here (since the value isn't ever invoked, but just passed to instanceof), but if you don't want to use it, you can just accept anything which has the shape of {prototype: {constructor: any}}:

interface Classlike {
  prototype: {constructor: any}
}

class Checker {
  /**
   * @param first a value
   * @param second the type of a value
   * @return true if "first" is an instance of type "second"
   */
  public instanceOf(first: unknown, second: Classlike): boolean {
    return first instanceof second.prototype.constructor;
  }
}

Playground link

Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • Very cool idea. Out of curiosity, what is the difference between `first instanceof second` and `first instanceof second.prototype.constructor` as you suggested? – Gili Oct 29 '20 at 05:05
  • Functionally, nothing, except that Typescript knows that constructor is `any`, which it knows it valid to test with instanceof, but we can pass an interface shape that will prevent non-classes from being passing type checking. This is more a concession to Typescript than anything else; I think that just specifying Function as the type is the better approach. – Chris Heald Oct 29 '20 at 07:20