-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
[bug] Prevent generic type information loss from type narrowing #40436
Comments
Here's a more complex example to show how the inference could work. I'm "emulating" the behavior I would like so I'm using class A<T = unknown> {
constructor(public value: T) {}
}
class B<T = unknown> {
constructor(public value: T) {}
}
const wrap = <R>(fn: () => R) => {
const returnValue0 = fn();
if (returnValue0 instanceof A) {
return returnValue0 as Extract<typeof returnValue0, A>;
// type-safe narrowing with `A`
}
// since `A` was narrowed out, we exclude it out
const returnValue1 = returnValue0 as Exclude<typeof returnValue0, A>;
if (returnValue1 instanceof B) {
return returnValue1 as Extract<typeof returnValue1, B>;
// type-safe narrowing with `B`
}
// since `B` was narrowed out, we exclude it out
const returnValue2 = returnValue1 as Exclude<typeof returnValue1, B>;
// at this stage we know for sure that the type of `returnValue` is
// `A` & `B` excluded: `Exclude<Exclude<R, A<unknown>>, B<unknown>>`
return undefined;
}; |
While this could provide better type safety, it can become hard to read it. It would be nice for this to have subtraction types. So instead of having |
I wrote a custom type-guard for interface Class<P extends any[], O> extends Function { new (...args: P): O; }
type GetClassInstance<C extends Class<any[], any>> =
C extends Class<any[], infer O> ? O : never;
const instanceOf = <A, C extends Class<unknown[], object>>(
thing: A,
Class: C,
): thing is A & GetClassInstance<C> => {
return thing instanceof Class;
}; class A<T = unknown> {
constructor(public value: T) {}
}
const fn = <T>(value: T) => {
if (instanceOf(value, A)) {
return value;
}
return undefined;
};
const test = fn(new A(42));
if (test) {
type valueType = typeof test.value; // number
}
However, it is still not what I would like to achieve, the previous example is not passing. |
I got a step closer to the desired result with. It fully preserves type information. type Class<
P extends readonly unknown[] = readonly unknown[],
R = unknown
> = {
new (...args: P): R;
};
type InstanceOf<C extends Class> =
C extends Class<readonly unknown[], infer O>
? O
: never;
type Select<U, A> =
U extends A
? U
: never;
type Narrow<A, C extends Class> =
any extends A
? Select<InstanceOf<C>, A>
: unknown extends A
? A & InstanceOf<C>
: Select<A, InstanceOf<C>>;
const instanceOf = <A, C extends Class>(
thing: A,
Class: C,
): thing is Narrow<A, C> => {
return thing instanceof Class;
}; To improve the developer experience, I believe that we would wise to have subtract types: const fn = <T>(thing: T) => {
if (thing instanceof A) return 'A';
// typeof thing = T -A
if (thing instanceof B) return 'B';
// typeof thing = T -A -B
if (thing instanceof C) return 'C';
return thing; // T -A -B -C
}; This becomes especially interesting if |
I'm just going to respond to the OP here because I'm not sure what's going on with the pages of comments that follow. What you should write is class A<T> {
constructor(public value: T) {}
}
const wrap = <T,>(value: T) => {
if (value instanceof A) {
return value as A<T>; // <-- type assertion
}
return undefined;
}; because you're assuming that any |
Thanks @RyanCavanaugh for your fast reply class A<T> {
constructor(public value: T) {}
}
const fn = <T,>(value: T) => {
if (value instanceof A) {
return value;
}
return undefined;
};
const a = new A(42);
const test = fn(a); We do know for sure that type testType0 = (A<number> & A<unknown>)['value']; // number
type testType1 = (A<number> & A<any>)['value']; // any |
I don't understand why this is not the case. If |
@RyanCavanaugh I think As far as I can tell this issue is a duplicate of #4742 or #28560, with the comments in #30161 being particularly relevant. If you have a value |
Oh sorry, I completely omitted to rename |
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
TypeScript Version: 4.0.2
Search Terms: type narrowing, intersection, property, any, type loss, property widening, generic type narrowing, instanceof, typeof
Code
This shows how ts fails to preserve generic types after narrowing the type with
instanceof
:The inferred result of
wrap
isT & A<any>
. Because ofany
's (orunknown
's) widening nature, there is type information loss (widening to unsafeany
on properties).Expected behavior:
Instead of using
&
in the inferred result, could we use a conditional type instead? At the moment, I do this by hand:Actual behavior:
TypeScript creates intersections, causing generic property type loss.
Playground Link: https://www.typescriptlang.org/play?jsx=0&ts=4.0.2#code/N4Ag9GIA4E4PYCMA2BTAtgWAFAhAYyQEMBnYkAQQB4AVAPhGG11zzgDtiAXGAVz07gwAFFB7IAlnhAA3Qkh4oAXCGoBKBgF8mILVm2sOnEAHcYhKCAC8IGrSGz5Slesv1GOZuIBmIe3IUg4oaEbHgocD7k6u7MzDAonDwwbDL+KADc2ri6WSDxickgPGwAJiheQSglmR4aNfrsXCCcKE3WpuZCbCjGFEIALABMqqr1Ht6+LVzRuZwAnlAoqY7UC0vW84sRza2cAHQOCungkCFz2ro5WKAQ0HCk4shLxHDynOLs+kSkFLYMuQYuLx+IIRGIkJJlgplGpNBdsA1DCYzBZrLY-I4YS43LkJhiAkEuCEwtsov8PLF8kkUocliQQGcGWQAKIAD24hH4NAANL8zrRaDVYldKQlqUVSuVKtULmMWI0jFMjO0UV0en0hiM5YEfEIlTMKc01lCUKtFlYjVsfEqDmljrc2Dw0AgUDB4VhdEA
Related Issues:
Related from far #37993
The text was updated successfully, but these errors were encountered: