-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Comments
What is the intent of |
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 |
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 |
@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 |
@weswigham the more complex example in the middle explains why that doesn't always work - I really don't want to keep the 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?) |
With the new example, you still have not answered @RyanCavanaugh question:
I would have written the type guard as: function isWidget(t: Thing<ThingTypes>): t is Thing<ThingTypes.Widget> {
return t.thingType === ThingTypes.Widget;
} |
@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 Normally, the resulting type 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` |
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. |
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed. |
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
Expected behavior:
Compiles without errors
Actual behavior:
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
The text was updated successfully, but these errors were encountered: