Skip to content

Superclass/subclass generic type inference produces different results depending on generic usage #39851

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

Closed
chanderson0 opened this issue Jul 31, 2020 · 5 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@chanderson0
Copy link

chanderson0 commented Jul 31, 2020

TypeScript Version: 3.9.2

Search Terms: generic subclass infer unused

Expected behavior: Inference of generics shouldn't depend on the usage of those generics in the superclass. Separately, the behavior of inferring generics is confusing: sometimes they're left at the base of the superclass, sometimes they're made narrower by subclasses - is this intentional?

Actual behavior: Behavior depends on how the types are used, producing inconsistent results.

Related Issues:

Code

// ----------------------
// Case 1: unused generic
// ----------------------

class A<T extends number> { }
class B extends A<1> { }

type AGeneric<T> = T extends A<infer U> ? U : never;

type Ag = AGeneric<typeof A>; // number
type Bg = AGeneric<typeof B>; // number (should be: 1)

const a = new A<2>();
const b = new B();

type ag = AGeneric<typeof a>; // 2
type bg = AGeneric<typeof b>; // number (should be: 1)

// --------------------
// Case 2: used generic
// --------------------

class X<T extends number> {
    foo(x: T) { }
}
class Y extends X<1> {}

type XGeneric<T> = T extends X<infer U> ? U : never;

type Xg = XGeneric<typeof X>; // never (should be: number)
type Yg = XGeneric<typeof Y>; // never (should be: 1)

const x = new X<2>();
const y = new Y();

type xg = XGeneric<typeof x>; // 2
type yg = XGeneric<typeof y>; // 1
Output
"use strict";
// ----------------------
// Case 1: unused generic
// ----------------------
class A {
}
class B extends A {
}
const a = new A();
const b = new B();
// --------------------
// Case 2: used generic
// --------------------
class X {
    foo(x) { }
}
class Y extends X {
}
const x = new X();
const y = new Y();
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

@stephanedr
Copy link

@chanderson0, A, B, X and Y are already types, so you should not use typeof.
Removing typeof:

type Xg = XGeneric<X<number>>; // number
type Yg = XGeneric<Y>; // 1

You can add a default for T (class X<T extends number = number> {...}) to allow type Xg = XGeneric<X>; // number.

However you still get number for B.
The reason is that you need to declare a member, or a method with an argument (as in X) or return value of type T, in order to "capture" T.
Adding a member (that you don't need to assign):

class A<T extends number = number> {
    private _?: T;
}

type Bg = AGeneric<B>; // 1
type bg = AGeneric<typeof b>; // 1

@chanderson0
Copy link
Author

chanderson0 commented Aug 1, 2020

Thanks for the clarification; my mistake for using typeof on a type. The "capturing" behavior is still confusing (did I miss documentation somewhere?), because it doesn't seem to be well respected. A few illustrative cases below:

The first case that's confusing to me is that extensions of the generic type don't seem to cause any capturing:

class A<T extends number = number> {
    foo<U extends T>(u: U): U {
        return u;
    }
}

class B extends A<1> { }

type AGeneric<T> = T extends A<infer U> ? U : never;

type Ag = AGeneric<A>; // number
type Bg = AGeneric<B>; // number (should be: 1)

Playground Link

And then there are several more behaviors that don't seem right. Note how in case 3, there's a whole new type that hasn't shown up anywhere.

// ----------------------
// Case 1: captured types
// ----------------------

class A<T extends string | { [k: string]: number } = string> {
    _t?: T;
}
class B extends A<{ one: 1 }> { }

type AGeneric<T> = T extends A<infer U> ? U : never;

type Ag = AGeneric<A>; // string
type Bg = AGeneric<B>; // { one: 1 }

// ---------------------
// Case 2: derived types
// ---------------------

type KeysOrString<T> = T extends string ? T : keyof T;

class X<T extends string | { [k: string]: number } = string> {
    _u?: KeysOrString<T>;
}
class Y extends X<{ one: 1 }> { }

type XGeneric<T> = T extends X<infer U> ? U : never;

type Xg = XGeneric<X>; // string
type Yg = XGeneric<Y>; // "one" (should be { one: 1 })

// ------------
// Case 3: both
// ------------

class M<T extends string | { [k: string]: number } = string> {
    _t?: T;
    _u?: KeysOrString<T>;
}
class N extends M<{ one: 1 }> { }

type MGeneric<T> = T extends M<infer U> ? U : never;

type Mg = MGeneric<M>; // string
type Ng = MGeneric<N>; // "one" | { one: 1 } (should be { one: 1 })

// And just to be sure...
type MDefinedGeneric = MGeneric<M<{ one: 1 }>>; // { one: 1 }

Playground Link

The original motivation for this was usage of the eventemitter3 types.

type Events = {
    one: [1],
    two: [2]
};

class MyEmitter extends EventEmitter<Events> { 
    foo() {
        this.emit('one', 1);
    }
}

type EventEmitterGeneric<T> = T extends EventEmitter<infer U> ? U : never;

type MyEmitterEvents = EventEmitterGeneric<MyEmitter>; // "one" | "two" (should be: Events)

Including type definitions: Playground Link

@stephanedr
Copy link

Look at the FAQ/generics.

In the FAQ they only mention to add a member of type T, whereas, by testing, we also see that a method argument or return value of type T is OK too.

I'm not an expert of TS, so my feeling (just my feeling) is that in all the cases you don't specify T directly, but a type derived from T (U extends T or T extends string ? T : keyof T).

However, with T extends string ? T : keyof T, Yg should be string | {[k: string]: number; } (no members of type T).
It seems TS considers it has captured T, whereas it is keyof T...

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Aug 25, 2020
@RyanCavanaugh
Copy link
Member

You're observing that generics behave differently depending on their variance, which is the intended behavior.

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants