TypeScript Version: 3.7.0-dev.20190823
Search Terms: type guard, refine, fail, assignment
Code
interface Map1<K, HK extends K> {
has(key: K): this is Map2<K, any>;
get(key: HK): any;
}
declare let map1: Map1<string, never>;
if(map1.has("foo")) {
let x = map1.get("foo"); // okay
}
interface Map2<K, HK extends K> {
has(key: K): this is Map2<K, any>;
get(key: HK): any;
// If you remove this line, Map2 behaves just like Map1
behavior(): HK;
}
declare let map2: Map2<string, never>;
if(map2.has("foo")) {
let x = map2.get("foo"); // fail: map is still Map2<string, never>
}
Expected behavior:
map2.has should refine the type just the same as map1.has does, regardless of the presence of other methods on the interface.
Actual behavior:
map2.has doesn't refine the type at all so long as behavior has the return type HK.
Playground Link:
playground
In the first case the predicate type is a subtype of source type, so it narrows to the predicate.
This is not true in the second case: the additional method make the predicate unassignable to the source because any is not assignable to never.
The method has an effect because it means the predicate does not produce a more precise type: the type of behaviour is less precise in the predicate type.
I was wondering if the problem wasn't related to assignability. I also tried has(this: any, key: K): this is Map2<K, any>, but that also has an issue even though Map2<K, any> is a more precise type than any.
As usual I don't have much useful to add to @jack-williams's analysis. Map2 is invariant on HK so it'd be unsound to narrow it to Map2<string, any>
The example above is a reduced version of:
// If T is a refinement of C (T !== C, T is a subtype of C), produce T, otherwise never.
type Refines<T extends C, C> = C extends T ? never : T;
interface Map2<K, HK extends K> {
has<RK extends K>(key: RK): this is Map2<K, Refines<RK, K> | HK>;
get(key: HK): number;
get(key: K): number | undefined;
}
declare let map2: Map2<string, never>;
if(map2.has("foo")) {
let x: number = map2.get("foo"); // fail: map is still Map2<string, never>
}
The issue goes away if I use Refines<RK | HK, K> or RK | HK in place of Refines<RK, K> | HK. I'm not sure if this is an unrelated issue compared to the above.
_TLDR_: K extends any ? never : any does not get eagerly simplified to any when relating to itself.
Not 100% sure on that, but what I think is happening:
First thing to note is that, ignoring has, Map2 is bivariant in HK because get is a method rather than a function property. With the addition of has, Map2 is covariant in HK.
This is most likely because type predicates are covariant, so this is Map2<K, Refines<RK, K> | HK1> is only related to this is Map2<K, Refines<RK, K> | HK2> when HK1 is related to HK2.
As to why Refines<RK | HK, K> and RK | HK restore bivariance of HK, my suspicion is that it's due to the erasure of RK to any when the generic signatures of has are compared during variance measurements.
Here is an inlined expansion of what I think is going on:
// corresponds to Refines<RK, K> | HK
interface FooCO<K, HK extends K> {
foo: (x: any) => HK | (K extends (any) ? never : (any));
}
declare const one: FooCO<string, never>;
declare const two: FooCO<string, 'foo'>;
const a: FooCO<string, never> = two; // error, not contra in HK
const b: FooCO<string, 'foo'> = one; // co in HK
// corresponds to Refines<RK | HK, K>
interface FooBI<K, HK extends K> {
foo: (x: any) => (K extends (HK|any) ? never : (HK|any));
}
declare const one2: FooBI<string, never>;
declare const two2: FooBI<string, 'foo'>;
const a2: FooBI<string, never> = two2; // contra in HK
const b2: FooBI<string, 'foo'> = one2; // co in HK
// corresponds to RK | HK
interface FooBI2<K, HK extends K> {
foo: (x: any) => (HK | any);
}
declare const one3: FooBI2<string, never>;
declare const two3: FooBI2<string, 'foo'>;
const a3: FooBI2<string, never> = two3; // contra in HK
const b3: FooBI2<string, 'foo'> = one3; // co in HK
So, getting to the point: I think that when relating HK | (K extends (any) ? never : (any)) to itself (or a sub/super type of HK) it's not simplifying the conditional type so it must be the case that HK is related to it's sub/super type. With the other two options the union of any is applied directly to HK, so HK ends up getting subsumed by any and the resulting measurement is bivariant.
If you change the definition of Refines to
type Refines<T extends C, C> = [C] extends [T] ? never : T
You should get the intended behaviour because the conditional type eagerly resolves to any in the measurement check which then subsumed HK.
And I'll just add this as a piece of relevant work #30461: it's not related to your specific bugs/questions, but it is related to your wider goal --- maybe it's interesting.
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.
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.
Most helpful comment
And I'll just add this as a piece of relevant work #30461: it's not related to your specific bugs/questions, but it is related to your wider goal --- maybe it's interesting.