-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Suggestion: Type guard generic types #4742
Comments
Just thinking about this a bit more. I guess this is not allowed for the same reason that this is not allowed: function myFunction<T>(item: T) {
const s = item as string; // error, neither T nor string is assignable to the other
} I guess it does make sense to not allow this the more I think about it and it won't be needed in most scenarios. I'm closing this issue. |
There are still valid cases where this can be helpful, especially when generic type extends from a union type. E.g class Cat { }
class Dog { }
class Node<T extends Cat | Dog> {
data: T = null
print() {
if (this.data instanceof Dog) {
// this.data now has the type Dog inside the block
}
}
} But the example doesnt seem to work today as typescript does not specialise the generic type even if it is a subset of the type it extends from |
there is another problem in the use of |
Accepting PRs. The root cause here comes from two factors. First is that given a (constrained) generic type parameter function clone<T extends Animal>(x: T) {
return new Dog(); // Unsafe: Dog is subtype of Animal, but is not a subtype of T
}
clone(new Cat()); // Example invocation of other subtype Second is that a type guard on an expression of type The rules do not combine favorably in the case of generics, though. We believe the correct fix is to take the apparent type of the generic type parameter when performing the assignability check in the second rule. |
Thinking about @RyanCavanaugh's two root causes, why should type guarding Cat with |
I have another use case for this. Simplified: interface A { type : string }
interface B extends A { f () : B }
function isB (x : A) : x is B {
return x.type === "B";
}
// Doesn't work.
function g <T extends A> (x : T) : T {
if (isB(x))
return x.f()
}
// Works, but lets you return an `A` if passed a `B`.
function h (x : A) : A {
if (isB(x))
return x.f(); The function |
Another use case: function foo<T extends string | number>(value: T) {
return typeof value === "number" && isNaN(value);
// on isNaN value still is "value: T extends string | number"
} |
It seems like it already sort-of works if I explicitly write a generic function that returns the type guard: class Cat { meow() { console.log("meow") } }
class Dog { woof() { console.log("woof") } }
class N<T extends Cat | Dog> {
data: T = null
print() {
const data = this.data
// This is fine
if (is(data, Dog)) {
return data.woof()
}
// Unfortunately, this is not. Looks
// like narrowing isn't working.
// return data.meow()
// However, you can work around the
// problem with this:
if (is(data, Cat)) {
return data.meow()
}
}
}
function is<T, TClass>(x: T, c: new () => TClass): x is T & TClass {
return x instanceof c
} Unfortunately, it looks like narrowing of the union doesn't work. If I add the explicit annotation |
Why is this issue still open? All the examples here compile fine already. |
What about more advanced scenarios? Like this: function foo<T>(a: T) {
const b = a;
if (typeof a === 'string') {
console.log(b.toLowerCase()); // error
}
} It doesn't compile, however wouldn't it be safe to assume that |
I found another example of this problem where this goes wrong.
Of course in this simple example |
@rolfvandekrol That is probably related to this: #21483, or at least it suffers from the same issues. |
So few answers and reactions to such an old problem... :С A similar problem: https://stackoverflow.com/questions/63594468/type-guard-dont-work-with-generic-params |
This is working as intended. Types are immutable, only variables can be narrowed, |
Playing with the OP's second example (and fixing some apparently unintended errors), I've found that it is optional part that causes the compile error. In other words, this compiles perfectly fine: interface Doubleable {
double(): void; // ✅ required
}
declare function isDoubleable(value: unknown): value is Doubleable;
class Wrapper<Value> {
act(value: Value) {
if (isDoubleable(value)) {
value.double();
}
}
} … while this doesn't: interface Doubleable {
double?(): void; // ❌ optional
}
declare function isDoubleable(value: unknown): value is Doubleable;
class Wrapper<Value> {
act(value: Value) {
if (isDoubleable(value)) {
value.double();
// ^^^^^^
// Property 'double' does not exist on type 'Value'. (2339)
}
}
} This happens not only in generic classes, but in generic functions as well, so I guess it is really a problem with generics. What's weird is that the error message says " interface Doubleable {
double?(): void;
}
function double(value: Doubleable): void {
value.double();
// ^^^^^^^^^^^^
// Cannot invoke an object which is possibly 'undefined'.
} … but in our cases it does something different. |
All these examples (except #4742 (comment)) are working now 😄 |
Would it be good to have type guarding on generic types? Or was this decided against?
Example 1
I realize this example could be rewritten without generics. It's just an example :)
Example 2
Here's a scenario I came across when creating a wrapper class:
Maybe a generic type with no constraint should be treated like an
any
type in this case? (Once type guards are fixed for any types -- See #4432)This suggestion is probably very low on the priority scale.
I couldn't find this issue discussed elsewhere so sorry if it was.
The text was updated successfully, but these errors were encountered: