Skip to content

Type guards on bound generic parameters don't compile #24935

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
spion opened this issue Jun 13, 2018 · 9 comments
Closed

Type guards on bound generic parameters don't compile #24935

spion opened this issue Jun 13, 2018 · 9 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@spion
Copy link

spion commented Jun 13, 2018

Seems like type guards on bound generic parameters don't compile - the compiler doesn't take the constraint into account.

TypeScript Version: 2.9

Search Terms:

generic type guard

Code

function isOne<T extends 1 | 2 | 3>(t: T): t is 1 { return t === 1; }

Expected behavior:

Compiles without errors

Actual behavior:

A type predicate's type must be assignable to its parameter's type.
  Type '1' is not assignable to type 'T'.

Playground Link:

http://www.typescriptlang.org/play/#src=function%20isOne%3CT%20extends%201%20%7C%202%20%7C%203%3E(t%3A%20T)%3A%20t%20is%201%20%7B%20return%20t%20%3D%3D%3D%201%3B%20%7D

Related Issues:

Could not tell

@RyanCavanaugh
Copy link
Member

What is the intent of T here? It's unnecessary

@mhegazy mhegazy added the Needs More Info The issue still hasn't been fully clarified label Jun 13, 2018
@spion
Copy link
Author

spion commented Jun 13, 2018

edit: The comment after this comment has a better example. This one describes that workarounds for the issue don't work perfectly in more extended cases.

Here is an example that is slightly less (but still quite) contrived (not the original code, but not a minimal nonsensical test case either):

type Widget = { value: number; }
const MyWidget: Widget = { value: 1 }

type Joojoo = { value: string }
const MyJoojoo: Joojoo = { value: '1' }

const AllWidgets = { MyWidget };
const AllJoojoos = { MyJoojoo };

enum ThingTypes {
  Widget = 1,
  Joojoo = 2
}


type IdOf<T extends ThingTypes> = 
T extends ThingTypes.Widget
    ? keyof typeof AllWidgets
    : T extends ThingTypes.Joojoo ? keyof typeof AllJoojoos : never

class Thing<T extends ThingTypes> {
  thingType: T;
  thingId: IdOf<T>;
}

function isWidget(t:Thing<any>): t is Thing<ThingTypes.Widget> {
  return t.thingType === ThingTypes.Widget;
}

function isJoojoo(t: Thing<any>): t is Thing<ThingTypes.Joojoo> {
  return t.thingType === ThingTypes.Joojoo;
}

  
function valueOf<T extends ThingTypes>(self: Thing<T>): T extends ThingTypes.Widget
    ? typeof AllWidgets[keyof typeof AllWidgets]
  : T extends ThingTypes.Joojoo ? typeof AllJoojoos[keyof typeof AllJoojoos] : never {
      
    if (isWidget(self)) {
      let tid = self.thingId;
      return AllWidgets[self.thingId];
    } else if (isJoojoo(self)) {
      return AllJoojoos[self.thingId];
    } else {
      throw new Error('What is this?')
    }
}
declare let v: Thing<ThingTypes.Widget>

let x = valueOf(v)

Since the guard can't narrow over the type parameter, it complains that the indexer type is wrong.

edit: thats because on the line let tid = self.thingId; the type is resolved as IdOf<T> & "MyWidget", and thats because thingType is resolved as T & ThingTypes.Widget

@spion
Copy link
Author

spion commented Jun 13, 2018

Maybe a better minimal example is

enum ThingTypes {
  Widget = 1,
  Joojoo = 2
}

type ThingData<T extends ThingTypes>
  = T extends ThingTypes.Widget ? string
  : T extends ThingTypes.Joojoo ? number
  : never;

class Thing<T extends ThingTypes> {
  thingType: T;
  thingData: ThingData<T>;

  // other methods follow.
}

function isWidget<T extends ThingTypes>(t:Thing<T>): t is Thing<ThingTypes.Widget> {
  return t.thingType === ThingTypes.Widget;
}

function isJoojoo<T extends ThingTypes>(t: Thing<T>): t is Thing<ThingTypes.Joojoo> {
  return t.thingType === ThingTypes.Joojoo;
}

But that can be worked around using any - however the above full example demonstrates that workaround is not perfect

@weswigham
Copy link
Member

@spion You want to write

function isOne<T extends 1 | 2 | 3>(t: T): t is (T&1) { return t === 1; } // note the intersection

this way your argument retains its relationship with T, but is also known to be a 1.

@spion
Copy link
Author

spion commented Jun 14, 2018

@weswigham the more complex example in the middle explains why that doesn't always work - I really don't want to keep the T because the conditional type IdOf<> wont be able to extract the right set of string keys so I can index into AllWidgets with it.

edit: Is there any reason why the type parameter needs to be kept though? Seems logical that bound type parameters should disappear when narrowed by type guards.

edit2: Oh, I think I see it. The type variable might be of an even narrower sub-type of whatever the typeguard is claiming, and we can't cast that information away.

edit3: Still, if its done explicitly in the type guard as a claim (we don't care if the type is even narrower, we want to discard the type variable) it should probably go away (edit4: well, if its in a covariant position.... I think?)

@mhegazy
Copy link
Contributor

mhegazy commented Jun 14, 2018

With the new example, you still have not answered @RyanCavanaugh question:

What is the intent of T here? It's unnecessary

I would have written the type guard as:

function isWidget(t: Thing<ThingTypes>): t is Thing<ThingTypes.Widget> {
    return t.thingType === ThingTypes.Widget;
}

@spion
Copy link
Author

spion commented Jun 14, 2018

@mhegazy See the conditional type in my second comment:

type IdOf<T extends ThingTypes> = 
T extends ThingTypes.Widget
    ? keyof typeof AllWidgets
    : T extends ThingTypes.Joojoo ? keyof typeof AllJoojoos : never

I believe a type variable is required there, so in that case the type guard you wrote will keep the T around and the conditional type wont be able to resolve the union-of-keys type.

Normally, the resulting type T & Something would not be a problem, but you can't use it to index in mapped types:

function valueOf<T extends ThingTypes>(self: Thing<T>): T extends ThingTypes.Widget
    ? typeof AllWidgets[keyof typeof AllWidgets]
  : T extends ThingTypes.Joojoo ? typeof AllJoojoos[keyof typeof AllJoojoos] : never {
      
    if (isWidget(self)) {
      let tid = self.thingId; // IdOf<self.thingType> = unresolved conditional type
      return AllWidgets[self.thingId]; // cant do this!
    } else if (isJoojoo(self)) {
      return AllJoojoos[self.thingId];
    } else {
      throw new Error('What is this?')
    }
}

edit: If we remove the type variable there, then the information would be lost in the code at the bottom of the second example:

declare let v: Thing<ThingTypes.Widget>

let x = valueOf(v) // says `Widget | Joojoo`, should know its just `Widget`

@spion
Copy link
Author

spion commented Jun 15, 2018

BTW for me personally this "bug" is really is low priorty now (needs a very specific interaction of type guards, conditional types and mapped types to reproduce, and I have a workaround using an alternative design) so feel free to ignore it. I am still a little curious to know whether there is any good reason to forbid a type guard from removing the type variable.

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

5 participants