question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

This narrowing typeguard effect bleeds into subsequent statments on a type with bivariant type-parameter

See original GitHub issue

TypeScript Version: 3.4.0-dev.201xxxxx (although 3.3 is also impacted) Search Terms: this type guards

If we create a type-guard that narrows this on a type that has a type-parameter that is present only in a bivariant position, the effect of the type guard persists outside of the guarded block.

Code

type GetKnownKeys<G> = G extends GuardedMap<infer KnownKeys>  ? KnownKeys: never;
interface GuardedMap<KnownKeys extends string> {
    get(k: KnownKeys): number;
    has<S extends string>(k: S): this is GuardedMap<S | GetKnownKeys<this>>;
}

declare let map: GuardedMap<never>;

map.get('bar') // err, as expected
if (map.has('foo')) {
    map.get('foo').toExponential(); // ok as expected
    if(map.has('bar')) 
    {
        map.get('foo').toExponential(); // ok as expected
        map.get('bar').toExponential(); // ok as expected
    }
        
    map.get('bar').toExponential(); /// OK!?!?! WHY ?!
    
}
map.get('bar') // OK ?!

Expected behavior: Type guard only impacts the guarded block. Actual behavior: The effect of the type guard bleads into all subsequent statements. (marked with OK!?!?! and OK?!)

Note: With strictFunctionTypes on, declaring get as get: (k: KnownKeys) => number; makes the code work as expected.

Playground Link: link

Related Issues: Similar to #14817

Found this while playing with a solution for #9619

Issue Analytics

  • State:open
  • Created 5 years ago
  • Reactions:1
  • Comments:10 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
RyanCavanaughcommented, Mar 18, 2019

Extremely good analysis @jack-williams

Here’s a simplified repro with fewer things going on

type Animal = { a: "" };
type Cat = Animal & { c: "" };
type Dog = Animal & { d: "" };

type Box<T> = {
    get(x: T): void;
}

declare function isCatBox(x: any): x is Box<Cat>;

function fn(arg: Box<Cat | Dog>) {
    arg.get({ a: "", d: "" }); // OK
    if (isCatBox(arg)) {

    }
    arg.get({ a: "", d: "" }); // Error
}

As for documentation, I have no idea how you would write this down in a way that anyone could find it unless they already knew what the doc said.

2reactions
jack-williamscommented, Mar 18, 2019

Are you sure about the difference between strict on/off and the type of w ? I am seeing the exact same type with my original code in both cases. Since get is a member method I would not expect strictFunctionTypes to impact GuardedMap but I could be wrong.

Sorry, I was assuming that get was written as get: (k: KnownKeys) => number;. If it’s written as a method then you’re stuck with bivariance irrespective of strictness mode so there is no way to see the different behaviour.

Is the fact that the flow type in subsequent statements is the union of both branch outcomes useful in this scenario? I don’t think so. Not sure I can carve out the situations when this is useful from the ones it is not though.

It’s probably not useful in this scenario, but across the board it’s generally useful and because it’s compositional it’s easier to implement. Where you see the benefit is when you have multiple branches, some of which return. In that case, gathering the flow types in a union is simple and effective. In your example, the problem only arises due to unsoundness in the type system.

Might be a corner of corner cases and not worth the hassle of dealing with it in any better way, but I though I might document it here in case someone else comes across it and see if anyone thinks it worth fixing 😃

It sort of feels like that, but maybe there is an easy fix. I think the problem is that any fix effectively amounts to a heuristic. I agree that documenting it, at the least, is worth the time.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Covariance and Contravariance in Generics - Microsoft Learn
A generic type parameter that is not marked covariant or contravariant is referred to as invariant. A brief summary of facts about variance...
Read more >
c# compiler error 'Parameter must be input safe. Invalid ...
You declared T as covariant (using the out keyword) but you cannot take covariant parameters: (MSDN). In general, a covariant type parameter can...
Read more >
The starting point for learning TypeScript
Find TypeScript starter projects: from Angular to React or Node.js and CLIs. ... Learn how to write declaration files to describe existing JavaScript....
Read more >
Understanding Covariance and Contravariance of Generic ...
In this article, we will examine what covariant and contravariant generic types are. We will explain what it means for a generic type...
Read more >
Kotlin generic variance modifiers | by Marcin Moskala
Kotlin is much safer than Java. Arrays in Kotlin have invariant type parameter. List interface has covariant type parameter, because it is immutable....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found