Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Wrong type inference, returns the base class type instead of the child class type #57286

Closed
denis-migdal opened this issue Feb 4, 2024 · 3 comments Β· Fixed by #57362
Closed
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@denis-migdal
Copy link

denis-migdal commented Feb 4, 2024

πŸ”Ž Search Terms

None

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________

⏯ Playground Link

Playground Link

πŸ’» Code

// BUG1:
{
	class Base {
		get self(): Base { return this }
	}
	class Child extends Base {
		override get self(): Child { return this }
		child = true;
	}

	function convert<T extends Base>(X: T) { // implicitly returns Base["self"] instead of T["self"]
		return X.self;
	}

	const value = convert( new Child() );
	// expected: typeof value = Child.
	// got: typeof value = Base.
	
	
	function convert2<T extends Base>(X: T): T["self"] {
		return X.self;
	}

	const value2 = convert2( new Child() );
	// typeof value = Child.
}

// BUG2:
{
	class BaseClass<V> {
		protected fake(): V { throw new Error("") }
	}

	class Klass<V> extends BaseClass<V>{
		child = true;
    }

	type Constructor<P extends BaseClass<number>> = new () => P;
	type inferTest<T> = T extends Constructor<infer P> ? P :never;

	type U = inferTest<Constructor<Klass<number>>> // U = Klass<number>
}
{
	class BaseClass<V> {
		protected fake(): V { throw new Error("") }
	}

	class Klass<V> extends BaseClass<V>{
		child = true;
    }

	type Constructor<V, P extends BaseClass<V>> = new () => P;
	type inferTest<V, T> = T extends Constructor<V, infer P> ? P :never;

	type U = inferTest<number, Constructor<number, Klass<number>>>
	// got: U = BaseClass<number>
	//expected: U = Klass<number>
}

πŸ™ Actual behavior

Bug1: It seems that when calling methods in generic function foo<T extends Base>(t: T) :

t.foo() // viewed as method Base.foo()
// instead of being viewed as T.foo() with T extends Base.

Bug2: For type inference, it seems that adding a generic parameters confuses TS.

πŸ™‚ Expected behavior

Cf code.

Additional information about the issue

Related to issue #57102

@jcalz
Copy link
Contributor

jcalz commented Feb 4, 2024

I don't know why you've reported two things in one issue, but in all of your examples, your child classes seem to be structurally identical to your base classes. Technically that means TS is within its rights to use the names interchangeably. Maybe for your examples you should add some distinguishing structure to your child classes just to avoid any confusion?

"Bug 1" is a widening of a generic to its constraint when indexing with a specific key. This is essentially the same thing as #33181, or at least both of these are caused by such widening. This isn't necessarily a bug: such a widening isn't incorrect in your example; it's just not as specific as you'd like. I'd also like to see this changed to be more generic, but I'd phrase it as a feature request and not a bug.

@denis-migdal
Copy link
Author

in all of your examples, your child classes seem to be structurally identical to your base classes. Technically that means TS is within its rights to use the names interchangeably. Maybe for your examples you should add some distinguishing structure to your child classes just to avoid any confusion?

Done.

"Bug 1" is a widening of a generic to its constraint when indexing with a specific key. This is essentially the same thing as #33181, or at least both of these are caused by such widening.

It is a little infuriating to see that the issues I spent days trying to find workarounds for are known for 4-5 years...

This isn't necessarily a bug: such a widening isn't incorrect in your example; it's just not as specific as you'd like.

I was wondering how foo<T extends Base>(): T["self"] was working with the "widening", but it seems it behaves like an as. Seems quite dangerous (cf below).

So the "widening" is forcing us to explicitly specify the generic function return type, which in turn can be error prone.

{
	class Base {

		get self(): Base { return this; }
	}

	class Child extends Base {

		override get self(): Child { return this; }
		child = true;
	}

	function foo<T extends Base>(e: T): T["self"] {
		//let self = e.self;
		let base = new Base();
		return base;
	}

	let ret = foo(new Child());
	let b = new Base();
	let c1 = b as Child; // ok
	let c2: Child = b;   // nok
}

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Feb 5, 2024
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 5, 2024
@RyanCavanaugh
Copy link
Member

Pointing out what I agree to be the problem:

class BaseClass<V> {
    protected fake(): V { throw new Error("") }
}

class Klass<V> extends BaseClass<V>{
    child = true;
}

// Replace BaseClass<V> with BaseClass<unknown> to un-repro
type Constructor<V, P extends BaseClass<V>> = new () => P;
type inferTest<V, T> = T extends Constructor<V, infer P> ? P :never;

type U = inferTest<number, Constructor<number, Klass<number>>>

declare let m: U;
// Should be OK, m: Klass
m.child;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants