Skip to content
New issue

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

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

Already on GitHub? # to your account

Allow generic parameters to propagate on returned type references. #23955

Closed
rjamesnw opened this issue May 8, 2018 · 10 comments
Closed

Allow generic parameters to propagate on returned type references. #23955

rjamesnw opened this issue May 8, 2018 · 10 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@rjamesnw
Copy link

rjamesnw commented May 8, 2018

Search Terms

Generic type parameters are lost upon return from a function.

Suggestion

In the case of the following:

class Test<Tx> { x: Tx; }
function Func<TClass>(type: TClass) { return type; }
var t = Func(Test);
var o = new t();

(https://goo.gl/YefQ6z)

I would have expected that the type of t is typeof Test<Tx>. Func() might be considered as a type of decorator of sorts, but it is a lot more complicated in my actual code, but this gets the idea across.

Use Cases

I'm creating a factory type system that is wrapped by a function (i.e. export Type = CreateFactory(base, class SomeType<T>(){...})). It uses type inference to figure out types and adds other types to the return type before exporting a property. Similar to:

namespace A {
  interface IFactory {
    'new'?(...args:any[]): any;
    'init'?(...args:any[]): any;
  }
  function CreateFactory<TClass extends new () => any, TFactory extends IFactory>(getClass:()=>[TClass, TFactory])
    : TClass & { originalType: TClass } & TFactory {
    var type = getClass();
    type['originalType'] = type;
    return <any>type;
  }

  export var Test = CreateFactory(() => {
    class Test<Tx> { x: Tx; }
    var factory = {
      new: function <Tx>(): Test<Tx> { return null; },
      init: function <Tx>(o: Test<Tx>) { return o; }
    }
    return [Test, factory];
  });
}

var o = A.Test.new<number>();

(https://goo.gl/fMtCPQ)

This works OK, and there is no issue with non-generic types. The problem is now with A.Test.originalType. I have no way to access the generic type signature anymore. Instead, I simply get typeof Test. I recommend the generic type signatures propagate properly. It may even help some people catch more bugs. This should allow me to do this:

    type TestType = typeof Test.originalType; // (currently this is only 'typeof Test' and the following fails)
    export interface ITest<Tx> extends InstanceType<TestType<Tx>> { }

Note: My actual code is a bit more complicated and allows inheritance chaining of the factories, which is why this pattern was the best suited for it.

Checklist

My suggestion meets these guidelines:
[x] This wouldn't be a breaking change in existing TypeScript / JavaScript code
[x] This wouldn't change the runtime behavior of existing JavaScript code
[x] This could be implemented without emitting different JS based on the types of the expressions
[x] This isn't a runtime feature (e.g. new expression-level syntax)

@jcalz
Copy link
Contributor

jcalz commented May 8, 2018

The syntax typeof XXX only works when XXX is a value, not a type. When you declare a class in TypeScript, it introduces both a value and a type with the same name. The value refers to the class constructor object (visible at runtime), and the type refers to the type of an instance of the class (erased at runtime). And despite having the same name, the type of the constructor is not the type of the instance. In your case, you have the type Test<T>, which is structurally equivalent to {x: T}, and the value Test, whose type, typeof Test, is structurally equivalent to something like new <T>() => Test<T>. Maybe you know this, but having two things with the same name can be confusing, so I'm trying to spell it out.

So typeof Test<T> doesn't work, since typeof Test<T> can only refer to the type of some value named Test<T>, and values are not generic (there is no runtime object named Test<T>). Are you asking for TypeScript to introduce generic values as in #17574? I'm not sure why you think you need that, since typeof Test is essentially the generic thing you want. You can do var o = new t<number>(); and it works, right?

What am I missing here?

@rjamesnw
Copy link
Author

rjamesnw commented May 8, 2018

I am aware, thanks. That was a simple example. The second code much better explains it. In essence I'm looking for this:

interface ITestNumber extends t<number> { }

Which of course does not work - I would need the type of generic t. As you said, if I can do new t<number> I don't understand why I cannot also create an interface from it, since obviously the type system understands newing it, so it must be there in the type system still. In the second code I posted there is no access to the original class type. This is a factory pattern, so not sure why you are focusing on using the 'new' operator. The new<T>() in my code is a function, not an operator.

To clarify, in my second code, the CreateFactory() method creates a factory signature from a class. The return is exported to a namespace with intersection types that add new(...args) and init(...args) with specific signatures (like constructors). I wouldn't need to do any of this if TypeScript simply supported adding new or similar to override base signatures on static types (much like how C# does for instances - yes, I know TS is not C#, but you get the idea). I cannot create static new(a:number) on a base type then static new(a:string) on a derived type, otherwise I wouldn't even create this post. The goal is to allow inheritance like class constructors where the parameters can be completely different from the base, hence the workarounds. I'm also trying to make sure the class type name is meaningful to end users in code completion (not anonymous), and I also want people to use the interface type instead of the class to push a convention that may better support dependency injection in the future (perhaps).

The case for #17574 is different from mine (the OP is talking about using type to define generic function types, where I am trying to return a generic type from a fucnction), but the issue of generic values may be related.

@jcalz
Copy link
Contributor

jcalz commented May 8, 2018

It still seems there's a disconnect about the difference between values and types when it comes to classes.

You can't do interface ITestNumber extends t<number> {} because t is not the name of a type.

You can do class CTestNumber extends t<number> {} because t is the name of a class constructor value, and the special syntax t<number> is actually saying "the parent class constructor is the object t and the instances of this class are those you would get when you call new t<number>(...)". I'm focusing on the new operator because a class value is a constructor function on which you call new.

I guess your problem isn't that typeof Test isn't generic (because it is a generic constructor function); it's that you can't pull the generic relationship between the constructor and the instance type out of the constructor type. TypeScript doesn't have higher kinded types, so you can't do something like this:

// doesn't work
type GenericInstanceType<C extends new <R>(...args: any[]) => any, T> = 
   C extends new <R>(...args: any[]) => (infer F)<R> ? F<T> : never;

where the (infer F)<R> would possibly mean "infer a type constructor F which applies to the type parameter R in this position". Then you could use

  // also doesn't work
  export interface ITest<Tx> extends InstanceType<TestType, Tx> { }

So maybe this issue is a request for higher kinded types in TypeScript? Or specifically, some way to refer programmatically to the stuff going on in the Bar<T> part of the special syntax class Foo<T> extends Bar<T>. Not sure.

@rjamesnw
Copy link
Author

rjamesnw commented May 8, 2018

JCalz, I hoped it was clear I know that by now. Of course you can't that's the point. ;) I was only trying to give an idea of what I was thinking, that is all.

you can't pull the generic relationship between the constructor and the instance type out of the constructor type

Correct, that's my main point. ;)

You made a good point about being able to use a class to inherit from a generic type, in which I could create an interface type from the class type:

declare class T<A> extends t<A> { }
interface ITest<A> extends T<A> { }

I think it may be an acceptable workaround, for now. 👍 :) Of course I shouldn't have to do this in the first place.

Perhaps it is a request for higher order types, not sure either. All I know is my idea is very simple to lay out. If I had this:

class A<T> { }

Means 'A' references a constructor AND a generic class type. Then if I do this:

var B = A;

B SHOULD act the SAME as A (type-wise). The constructor AND the generic type should be copied over. I don't see why this is hard to understand? ;) It's straight forward; however, I'm not sure how straight forward it would be to implement. I don't think anyone would object to this behaviour. It only adds more richness and consistency to the system I think.

@jcalz
Copy link
Contributor

jcalz commented May 9, 2018

If you're saying that var B = A (when A is a constructor function) should introduce a type named B, which is equivalent to the type named A, I think there will be objections. It would definitely be a breaking change, because it would add a whole bunch of new possibly-conflicting types to existing code.

At this point I should bow out and wait to see what real TypeScript language designers/maintainers have to say, since I'm just an annoying interested bystander.

@weswigham
Copy link
Member

If you're saying that var B = A (when A is a constructor function) should introduce a type named B, which is equivalent to the type named A, I think there will be objections. It would definitely be a breaking change, because it would add a whole bunch of new possibly-conflicting types to existing code.

We've actually been considering binding var a = b like an alias (meaning like import a = b) in JS for a bit now, as it turns out people enjoy writing things like

const MyClass = module.exports = class Foo {}

and then referring to MyClass all over. We'd have to do a lot of compat testing and thinking before we decided to do it in TS, though. I think that discussion is mostly unrelated to the OP's request though - it looks like he wants a partially inferred type - something where we've done an inference pass up to a point, but then left some type parameters bare so they can be inferred in another call.

@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jul 18, 2018
@mhegazy
Copy link
Contributor

mhegazy commented Jul 18, 2018

We currently have no way of letting generic parameters "escape" from an instantiation. the system assume that all parameters will be accounted for. i

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@aleclarson
Copy link

aleclarson commented Sep 24, 2018

I believe this issue should be considered higher priority. Generic classes should not be losing type information. Playground link

Related: #13798

My use case

Declaring/returning a generic class in an IIFE, which also contains other type declarations meant to be used only by said generic class.

@kresli
Copy link

kresli commented May 15, 2019

Seems like not just generic type is lost but constructor too

export function Wrapper() {
    return class {
        title!: string;
  };
}


function bindProperty<Prop extends ReturnType<typeof Wrapper>>(Prop: Prop) {
  class SyncedProp extends Prop {} // <-- why the Prop is NOT a constructor type?
  // Type 'Prop' is not a constructor function type.ts(2507)
}

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

7 participants