Repository navigation
TS1092: Type parameters cannot appear on a constructor declaration #10860
Copy link
Copy link
Closed
Labels
Working as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
Description
TypeScript Version: nightly (2.1.0-dev.20160906)
Code
// (A) declare Wrapper class directly - THIS DOESN'T WORK
// The errors below are all 'TS1092: Type parameters cannot appear on a constructor declaration'
declare class Wrapper {
constructor<TR>(wrapped: () => TR): NullaryWrapper<TR>; // ERROR
constructor<TR, T0>(wrapped: ($0: T0) => TR): UnaryWrapper<TR, T0>; // ERROR
constructor<TR, T0, T1>(wrapped: ($0: T0, $1: T1) => TR): BinaryWrapper<TR, T0, T1>; // ERROR
invoke(...args: any[]): any;
}
// (B) declare Wrapper as static side + instance side interfaces - THIS WORKS FINE
// This is equivalent to the class declaration in (A), but in this case it works without errors.
declare let WrapperConstructor: {
new<TR>(wrapped: () => TR): NullaryWrapper<TR>;
new<TR, T0>(wrapped: ($0: T0) => TR): UnaryWrapper<TR, T0>;
new<TR, T0, T1>(wrapped: ($0: T0, $1: T1) => TR): BinaryWrapper<TR, T0, T1>;
}
// Some strongly-typed versions of the Wrapper shape for specific arities
interface NullaryWrapper<TR> extends Wrapper { invoke(): TR; }
interface UnaryWrapper<TR, T0> extends Wrapper { invoke($0: T0): TR; }
interface BinaryWrapper<TR, T0, T1> extends Wrapper { invoke($0: T0, $1: T1): TR; }
// Some functions to be wrapped
const f0 = () => 42;
const f1 = (s: string) => 42;
const f2 = (s: string, n: number) => 42;
// Create some wrappers using the Wrapper class - DOESN'T WORK
new Wrapper(f0).invoke; // ERROR
new Wrapper(f1).invoke; // ERROR
new Wrapper(f2).invoke; // ERROR
// Create some wrappers using the WrapperConstructor var/interface - WORKS NICELY
new WrapperConstructor(f0).invoke; // () => number
new WrapperConstructor(f1).invoke; // ($0: string) => number
new WrapperConstructor(f2).invoke; // ($0: string, $1: number) => numberI'm trying to infer a strongly-typed shape for the class instance from the arguments passed to the constructor. There is no runtime shenanigans, this is purely for improved type-checking.
Are the TS1092 errors in the code at (A) above really necessary, given that the equivalent code at (B) works fine? I'd rather be able to write the Wrapper class directly than use the WrapperConstructor workaround of a var/interface combination.
They do appear to be equivalent typewise, but the (B) version seems a bit hacky. And if the compiler can accept (B), couldn't it also allow (A)?
Or is there a better way to do what I'm trying to do here?
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Working as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
Activity
Arnavion commentedon Sep 12, 2016
(A) makes no sense even without the type parameters. Constructors can't have a return type annotation because constructors don't return anything, and even if they did it would be the same class as what they're defined in.
The error about type parameters is just hiding the error about return annotations:
error TS1093: Type annotation cannot appear on a constructor declaration.For the same reason it doesn't make sense for constructors to have type parameters. The type parameters would go on the class they're a part of.
yortus commentedon Sep 12, 2016
Arnav Singh (@Arnavion) (A) does make sense - it's the same thing as (B). If (A) made no sense, then neither should (B), but that compiles fine.
(A) is returning an instance of the same class, just with more specific compile-time typing. It works at runtime, and it also works using the
interfaceapproach in (B) which amounts to the same thing.BTW constructors do return something, even in ES6. TypeScript even provides intellisense for their return type. It's just that the compiler applies different rules to
class { constructor(...)}than it does to the equivalentinterface { new(...) }.Arnavion commentedon Sep 12, 2016
I was going to edit this in right before you answered:
Also, while TS does allow you to write (B), you can't implement it in the way your type signatures intend at runtime either.
Of course both of these may change depending on how #7574 is fixed, i.e., if TS did allow you to return arbitrary things from constructors.
yortus commentedon Sep 12, 2016
To be clear, the code does not return arbitrary things from the constructor, it returns valid instances of the
Wrapperclass. The only difference is the compile-time type-checking of the instances, which are given a more specific shape, but one that is still compatible with the class. The compiler can deal with this just fine, as (B) shows. It just won't allow the equivalent formulation (A).Can you elaborate on this? The implementation I'm using with (B) works fine.
Arnavion commentedon Sep 12, 2016
Yes, I understand that. I assume your
invokeis something likefunction (...args) { return wrapped(...args); }, and you're trying to get additional typing by introducing interfaces corresponding to the wrapped function.I'm saying you cannot implement (B), i.e. the actual implementation of
AltWrapper, in TS. I assume you have a JS implementation and you're just trying to model it in TS.Now, if you're able to change the implementation, you could switch from
class Wrapper { constructor(func); invoke(); }tofunction Wrapper(func): NullableWrapper<>and other overloads, whereNullableWrapper, etc are functions themselves with the same type parameters. That can be modeled in existing TS just fine.yortus commentedon Sep 12, 2016
Arnav Singh (@Arnavion) here's a working implementation of (B). It's 100% TypeScript, works at runtime, and achieves the desired goal of getting compile-time type-inference from the arg passed to the constructor:
The
WrapperConstructor+Wrapperinterfaces are just the decomposed static-side and instance-side of a class, no more and no less. I've provided the runtime implementation as a class expression.I'd prefer to just be able to write the equivalent class directly without having to split up into static/instance-side interfaces just to get it to compile. Note the implementation of the class is
identical to the class expression used above, except the constructor has some compile-time-only overloads with the appropriate annotations.
yortus commentedon Sep 12, 2016
I've been trying to find a page that I think was in the handbook which showed that you could always rewrite a class declaration as a pair of interface declarations, one for the static side and one for the instance side. I've only found this, with an example
ClockConstructor+ClockInterface.There's a purely mechanical transform between these two formulations of a class type, and I thought they were supposed to be more-or-less identical to the type system. That's why I'm suggesting that the two formulations should work the same in the OP here. Either they should both work (I'd prefer this), or they should both give the same error.
Arnavion commentedon Sep 12, 2016
Right, you're accepting a
Functionparameter and asserting that you're using it correctly. I meant you couldn't write a type-safe implementation.Yes, I already know you can do this. But usually when one does this the static side only has one construct signature (that returns the same type as the instance side), or multiple construct signatures that return the same type as the instance side.
Personally I think TS is being lenient by allowing you to have construct signatures targeting arbitrary types in the first place, atleast until #7574 is implemented. It makes no sense in general.
But anyway, I get your point. No more argument from me.
RyanCavanaugh commentedon Sep 12, 2016
The reason you can't write type parameters on constructors is that there's no place to specify them in a
newinvocation of a generic class:The main difference between a
classand its static decomposition is that we believe the former has a unified return type ofnew, but not the latter. For an actual ES6 class this is generally a safe assumption, but older JS code can behave arbitrarily weirdly when invoked withnewso we allow mixed return types fromnewfor the sake of representing existing libraries.yortus commentedon Sep 13, 2016
Thanks for explaining Ryan Cavanaugh (@RyanCavanaugh). I don't mind sticking to the decomposed approach.
Interestingly the point about having no place to specify the type parameters on a
newinvocation equally applies to the decomposed approach, but the compiler works with it just fine and it's actually very useful. The type parameters must be inferred from the arguments, since you can't write them explicitly (but inference is the whole point of doing this anyway).If the OP problem could be solved with a type parameter on the class I'd do that, but I don't think it's possible because of the variadic number of type parameters needed to capture the various function signatures.
So is there any guidance here? Would #5453 provide a way to do this? Something like:
yortus commentedon Sep 13, 2016
Slightly offtopic since the technique is not used here, but why does TypeScript assume ES6 classes have a unified return type from
new, differently from ES5 constructor functions? They both support returning something other thanthis, even a completely different (object) type. The semantics didn't change much w.r.t. constructor return values from ES5 --> ES6.RyanCavanaugh commentedon Sep 13, 2016
This is definitely technically true, but it's really hard to imagine why you'd bother writing an ES6 class if you were going to
returnsomething else out of the constructor. Really something that returns a different object shape from its constructor isn't a "class" as most people classically imagine it. ES5 was a lot more of a wild-west where library authors would often[ish] pretend thenewoperator wasn't there, or vice versa. TL;DR pragmatism.yortus commentedon Sep 13, 2016
Ryan Cavanaugh (@RyanCavanaugh) here is a real example of returning something else from the constructor in an ES6 class. The example there creates a callable object using a constructor, preserving
instanceofbehaviour as well. It's effectively a way of subclassingFunction, and AFAIK there's no other practical way to do this (well you can hack the prototype chain but I'm skipping that).Of course you can create an ES6 class that subclasses
Function. But that's mostly useless in practice, because the subclass' callable body cannot be a closure and therefore cannot close over any of properties of the class instance or objects passed to the constructor. That's because of the requirement to delegate tosuper(...)which is theFunctionconstructor, which always evaluates its source code in global scope.billba commentedon May 11, 2017
Now that we have default generics, I'd like to open this back up, because now the type parameters don't need to be specified in a
newinvocation of a generic class.My situation is that I'm doing some functional programming fu where I'd like to pass a chain of functions which represent transforms.
I don't care what types B-D are, so long as they form a consistent chain. When the compiler can't infer them, it can use the defaults. In my actual code I'd probably use a more constrained default than "any" but same idea applies.
I propose that TypeScript allow generic type parameters on constructors when they have defaults. I'm afraid I'm not enough of a compiler geek to provide a more detailed proposal, but maybe someone else can help out?