#New B where class B extends class A produces A not B (and cannot be assigned to B)

1 messages ยท Page 1 of 1 (latest)

hollow dragonBOT
#
sandiford#0

Preview:```ts
class A {
__kind = "A" as const
}

function create2<B extends typeof A>(B: B): B {
// new B should result in type B, surely
const b = new B() // but b is type A
return b // Type 'A' is not assignable to type 'B'.
// 'B' could be instantiated with an arbitrary type which could be unrelated to 'A'.
...```

civic arrow
#

Seems to be an issue with the error message

#

I fixed a mistake

hollow dragonBOT
#
sandiford#0

Preview:```ts
class A {
static __kind = "A" as const
__kind = "A" as const
}

function create<BClass extends typeof A>(
B: BClass
): InstanceType<BClass> {
type B = InstanceType<BClass>
// new B should result in type B, surely
const b = new B() // but b is type A
...```

teal plover
civic arrow
#

create an instance of B and return in

#

I can fix it with a type cast

#

it's just weird that it gives type A from new B()

teal plover
#

What's B

civic arrow
#

any class

teal plover
#

Where is it defined

civic arrow
#

that extends A

#

Context: making a framework, A is internal, B is provided by future developer

teal plover
#

You're conflating values and types in your code.

civic arrow
#

the function makes new B, proxies it, and returns it

civic arrow
teal plover
#

Both.

civic arrow
#

OK so where is the issue in the second one?

hollow dragonBOT
#
sandiford#0

Preview:ts ... function create<BClass extends typeof A>( B: BClass ): InstanceType<BClass> { type B = InstanceType<BClass> const b = new B() return b as B } ...

civic arrow
#

TS allows this

teal plover
#

Don't know where to start...

civic arrow
#

Thanks

teal plover
#

I'll just try to write a solution...

civic arrow
#

does this help?

#

This works, so maybe you misunderstood what I'm doing?

hollow dragonBOT
#
sandiford#0

Preview:```ts
// framework code
class A {
static __kind = 'A' as const
__kind = 'A' as const
}

function create<BClass extends typeof A>(B: BClass): InstanceType<BClass> {
type B = InstanceType<BClass>
const b = new B() // type A not B
return new Proxy(b, {}) // requires cast
...```

teal plover
#
function create <T extends A> (constructor: new () => T): T {
  return new constructor()
}
civic arrow
#

that's cool

#

I still think that my way should work though

fiery steeple
#

Your code is wrong

civic arrow
#

why?

teal plover
#

Tbh there's a lot wrong with what you posted...

fiery steeple
#

typeof A is the constructor of the class A

#

So when you do new B() that gives an instance of the class

civic arrow
#

right

fiery steeple
#

But your function is declared to return B, which is a constructor.

civic arrow
#

It's javascript's fake classes throwing me off

teal plover
#

It's not that..

fiery steeple
#

It has nothing to do with fake classes.

#

When you have a class Animal, typeof Animal is the equivalent to AnimalConstructor.

teal plover
#

You also redefined B in your function from a value to be a type only to try to use that type as a value in the next line as if the type declaration did anything during runtime....

#

That's probably the biggest issue with all this, just tells me that you fundamentally misunderstand... many things...

fiery steeple
#

Your original code translates to:

// remember typeof Animal is the same as AnimalConstructor
function create<TConstructor extends AnimalConstructor>(Ctor: TConstructor): TConstructor {
  const instanceOfT = new Ctor()
  return instanceOfT // instanceOfT is clearly not TConstructor
}
civic arrow
#

I didn't redefine B. B variable and B type are separate

#

Variable and types live in separate spaces

#

do they not?

#

TS thinks B is used when it is not

teal plover
#

Ah OK well then maybe a less confusing choice of variable names would communicate intent better...

fiery steeple
#

The error message is pretty misleading though.

hollow dragonBOT
#
sandiford#0

Preview:```ts
// framework code
class A {
static __kind = 'A' as const
__kind = 'A' as const
}

function create<BClass extends typeof A>(B: BClass): InstanceType<BClass> {
type BType = InstanceType<BClass>
const b = new B() // type A not B
return new Proxy(b, {}) // requires cast
...```

civic arrow
#

yeah I get the issue. In theory typeof A is a type of class. But in JS classes are not real. So it's really the type of a function

fiery steeple
#

JS classes are real, and your issue has nothing to do with JS classes... this is a TS error.

civic arrow
#

I tried it an Kotlin (typed language), and it just doesn't allow me to use a class as a value to begin with, so there is not typeof A

#

JS classes aren't real, they are just sugar for constructor functions

fiery steeple
#

typeof A is in the type space, it has nothing to do with JS.

#

JS classes are very real, there are literally features you can only do with JS classes, for example private fields.

civic arrow
#

the constructor func is JS. typeof is getting the type of the function, instead of a type of a class, which doesn't exist

#

Hmm perhaps. But they are still backed by old school JS like constructor functions

fiery steeple
#

Nope

#

You are free to read the specs, but anyways that has nothing to do with the issue at hand.

civic arrow
#

Well I have a solution to the issue, so thanks

fiery steeple
#

As you said types (which is TS) and values (which is JS) live in two different spaces, and your problem was clearly your type not the value.

civic arrow
#

yep

#

Coming from doing OOP programming in typed languages, <B extends A> ... new B(), really sounds like B is a class that extends A, and new B() will be type B

#

but it makes sense if you think of typeof A as a function retuning A not a class <- which is why I point the finger at JS for that thing : )

teal plover
#

๐Ÿ‘

civic arrow
#

I like you thinking : )

fiery steeple
#

It's a little bit of confusing yeah.

civic arrow
#

@ts-ignore them away, something is the answer TBH ๐Ÿคฃ

#

So does typeof A contain info about the prototype of A too?

fiery steeple
#

The "an instance of Animal has type Animal" and "the Animal class is typeof Animal" trips people up a lot.

fiery steeple
#

But if you think about it, it makes sense.

civic arrow
#

but as I said, Dog extends typeof Animal, not returing Dog, is kinda weird

#

I think we could think of Dog extends Animal, meaning that Dog is a class that extends Animal. But it's been implemented as constructor of Dog extends constructor of Animal

fiery steeple
#

Because you are still thinking Dog is an instance of a class, it's not, it's the class itself

civic arrow
#

nope

#

I am thinking that Dog is a class

#

if Dog is a class that extends class Animal, new Dog() returns dog: Dog

#

if Dog.constructor extends Animal.constructor, then new Dog (Dog.constructor()) returns dog: Animal

fiery steeple
#

When you new() on a constructor, you get one instance of it.
When you new() on a AnimalConstructor, you get one instance of InstanceType<AnimalConstructor>, which is Animal.
When you new() on a X, you get one instance of InstanceType<X>.

civic arrow
#

When you new() on a AnimalConstructor, you get one instance of Animal, or one instance of InstanceType<Animal>

#

this?

#

When you new() on a AnimalConstructor, you get one instance of Animal, or one instance of InstanceType<typeof Animal>

fiery steeple
#

I wrote it wrong, edited.

civic arrow
#

or this?

#

ahh

#

yeah I agree with you

#

I don't think you got my point : )

fiery steeple
#

So in your example, Dog extends typeof Animal, and you new Dog(), you get an instance of InstanceType<Dog>.

civic arrow
#

no you get Animal

fiery steeple
#

Sure.

civic arrow
#

which is why I posted it

fiery steeple
#

But your original code doesn't return Animal, your original code returns Dog.

civic arrow
#

It wants to yes

#
class Animal {}
class Dog extends Animal {}
const dog = new Dog() // Dog
type AnimalClass = typeof Animal
function make<DogClass extends AnimalClass>(Dog: DogClass) {
  const dog = new Dog() // Animal
}
#

This was my thinking

fiery steeple
#

Correct, and the return type is clearly not DogClass.

#

Your original code has the return type DogClass.

civic arrow
#

I know

#

I fixed that in the second post

hollow dragonBOT
#
sandiford#0

Preview:```ts
// framework code
class A {
static __kind = 'A' as const
__kind = 'A' as const
}

function create<BClass extends typeof A>(B: BClass): InstanceType<BClass> {
type BType = InstanceType<BClass>
const b = new B() // type A not B
return new Proxy(b, {}) // requires cast
...```

civic arrow
#

here

fiery steeple
#

Ah, I completely missed your second post.

#

My bad.

civic arrow
#

haha it's OK

#

thinking of typeof Animal as the type of class Animal is the problem

type AnimalClass = typeof Animal
function make<DogClass extends AnimalClass>(Dog: DogClass) {
  const dog = new Dog() // Animal
}

But it makes sense thinking of it as constructor function

type AnimalConstructor = typeof Animal // returns Animal
function make<DogConstructor extends AnimalClass>(Dog: DogConstructor) {
  // DogConstructor extends AnimalConstructor, so must also return Animal
  const dog = new Dog() // Animal
}
fiery steeple
#

It has nothing to do with that.

civic arrow
#

Oh come on

fiery steeple
#

Your example can be reduced to this

civic arrow
#

shoot

hollow dragonBOT
#
function create<Callback extends () => string>(callback: Callback): ReturnType<Callback> {
  const result = callback()
  return result
//^^^^^^
// Type 'string' is not assignable to type 'ReturnType<Callback>'.
}
civic arrow
#

confusing me

#

I don't know if I've ever used T extends [function] (on purpose)

#

what is ReturnType<Callback> ?

#

It looks like it should be string

fiery steeple
#

Nope, it could be a subset of string.

civic arrow
#

ah right

#

could be 'foo'

fiery steeple
#

Yeah.

civic arrow
#

But, I think we are both right. Because you are having to describe the issue in terms of functions. I am saying you have to think of it as a function

#

If typeof Animal was not a function, but a different understanding of a class, then it might work differently

fiery steeple
#

Even though the correct type for result should be ResultType<Callback>, it's not very useful and TS instead gives it a type of string.

#

So at the return site string is not assignable to ResultType<Callback> anymore.

civic arrow
#

hmm

fiery steeple
#

The same fix also fixes this problem

hollow dragonBOT
#
function create<T extends string>(callback: () => T): T {
  const result = callback()
  return result
}
civic arrow
#

yeah

#

Do you agree with how TS handles it?

fiery steeple
#

Not entirely sure, I mean soundness also isn't a goal for TS

civic arrow
#
function create<Callback extends () => string>(callback: Callback): ReturnType<Callback> {
  const result = callback() as ReturnType<Callback>
  return result
}
#

sure

#

technically the type of result is ReturnType<Callback>, extending string

#

and that would make the code work without casting.

But it also would be awkward in other situations

#

ReturnType<Callback> == T extends string

#

But we haven't created T, so that could be awkward for TS to represent

fiery steeple
#

Yeah I wouldn't surprised if it's a limitation.

#

And I guess for the most part, this situation rarely comes up.

civic arrow
#

Well, most things are doable, but it's a question of effort

#

I think casting is the simplest fix

fiery steeple
#

Or do the non function generic ๐Ÿ˜„

fiery steeple
#

Yeah.

civic arrow
#

It's safer. But also harder to understand for newer people I think

#

well its not that hard

#

so yeah its good

fiery steeple
#

Hmm debatable, I would say typeof A is even more confusing.

#

Although, that solution doesn't work if you need to also care about the constructor arguments.

civic arrow
#

if you have constructor args, do you need to list them?

#

Argument of type 'typeof AppStore' is not assignable to parameter of type 'new () => AppStore'.
Types of construct signatures are incompatible.
Type 'new (foo: string) => AppStore' is not assignable to type 'new () => AppStore'.
Target signature provides too few arguments. Expected 1 or more, but got 0.ts(2345)

#

(...args: any[])

#

I guess

#

to accept anything

fiery steeple
#

Yeah you would need to list them, and ...args: any[] is probably a bad idea.

civic arrow
#

why so?

#

A derived class could have any constructor. So you would need to accept a constructor with any arg list

#

unless there was a situation where you required a certain form, of course

fiery steeple
#

But inside your create, you are only calling new Ctor(), which means that class can't just have any constructor, it has to have a constructor that works without any argument.

civic arrow
#

ahh

#

yeah

teal plover
#

How are you going to call a function if you don't know its parameters?

#

Are the parameters also going to be passed to create?

civic arrow
#

So in this case we actually want to forbid any constructor args

teal plover
#

Maybe let the user supply a factory callback.

fiery steeple
#

Otherwise you would be forcing all derived classes (or at least the ones that work with your create) to have a parameterless constructor.

civic arrow
#

yep

#

actually if that is a concern, I just let them pass an instance of Dog

#

and skip the whole trouble

teal plover
#

Lol

civic arrow
#

yeah

civic arrow
#

No actually I can't do that, but it would not be session safe : )

#

the callback works though I think

#

!resolved

#

Thanks for the assistance

civic arrow
#

Can I differentiate between new () => Foo and () => Foo in JS?

fiery steeple
#

IIRC no simple way last I checked.

civic arrow
#

A.hasOwnProperty('prototype') detects whether it can be newd. But TS treats () => T as not newable.