镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Inferring Intersection Types #8911

Description

@leeavital

TypeScript Version:

1.8.10

Code

// two interfaces
interface A {
  a: string;
}

interface B {
  b: string;
}

// a type guard for B
function isB(toTest: any): toTest is B {
  return toTest && toTest.b;
}

// a function that turns an A into an A & B
function union(a: A): A & B {
  if (isB(a)) {
    return a;
  } else {
    return null;
  }
}

Expected behavior:

I would expect this to compile successfully.

Actual behavior:

test.ts(15,12): error TS2322: Type 'A' is not assignable to type 'A & B'.
  Type 'A' is not assignable to type 'B'.
    Property 'b' is missing in type 'A'.

Inside the if-block, tsc has "forgotten" about the A-ness of a. This is a useful pattern if b is an expensive property to compute and you want to do different things if b is or is not present.

Activity

  1. changed the title [-]Inferring Union Types[/-] [+]Inferring Intersection Types[/+] on Jun 1, 2016
  2. kitsonk commented on Jun 1, 2016

    @kitsonk
    Contributor

    Type guards don't magically widen types. The only narrow types. You made it clear that a is only A. Just because it has excess properties, doesn't make it A & B.

    If you wanted it to work, you would have to let the compiler know that a could possibly be A & B:

    // two interfaces
    interface A {
      a: string;
    }
    
    interface B {
      b: string;
    }
    
    // a type guard for B
    function isB(toTest: any): toTest is B {
      return toTest && toTest.b;
    }
    
    // a function that turns an A into an A & B
    function union(a: A | (A & B)): A & B {
      if (isB(a)) {
        return a;  // compiler correctly assumes `a` is not just `A` but is `A & B` and
                   // gives no error
      } else {
        return null;
      }
    }
  3. leeavital commented on Jun 1, 2016

    @leeavital
    Author

    Hm. Is there an issue with soundness?

    I know that a is an A because it's declared so in the signature. I know that it's also a B because the type guard says so. If something is an A and is also a B is it not safe to say it's an A & B?

  4. kitsonk commented on Jun 1, 2016

    @kitsonk
    Contributor

    Yes, but you said a is only A, or that you are contracting to only deal with an A structure with the a parameter. I guess with flow control, inside of the scope of the guard, the way you originally wrote it, it should be the new bottom type never, as you have said it is not A anymore. TypeScript is being permissive there at the moment. I haven't looked at what it would do under flow control though, but the TypeScript team would know better than I.

  5. leeavital commented on Jun 1, 2016

    @leeavital
    Author

    Why does it have to stop being an A ?

  6. weswigham commented on Jun 1, 2016

    @weswigham
    Member

    Lee Avital (@leeavital) Rewrite your type guard like so for the behavior you want:

    interface A {
      a: string;
    }
    
    interface B {
      b: string;
    }
    
    // a type guard for B
    function isB<T>(toTest: T): toTest is (T&B) {
      return toTest && (toTest as any).b;
    }
    
    // a function that turns an A into an A & B
    function union(a: A): A & B {
      if (isB(a)) {
        return a;
      } else {
        return null;
      }
    }

    TS only narrows a type to a subtype - it never narrows 'across', as it were.

  7. ahejlsberg commented on Jun 1, 2016

    @ahejlsberg
    Member

    With the nightly build compiler, the type of a is never under the isB(a) type guard. In 1.8 we didn't have a never type and we'd instead revert to the declared type.

    The operating principle for type guards is that they only "narrow" the type, i.e. they only make the type more specific. Type guards never change the type to something unrelated such as from A to B. If they did you'd end up with odd effects such as a not being assignable to itself.

    I think you're suggesting we should allow "narrowing across" to unrelated types and then produce an intersection of the declared type and the tested-for type (i.e. A & B in your example). Effectively this would guarantee that the resulting type is always a more specific type and always assignable to the declared type. The flip side is that it would produce nonsense types for nonsense type guards instead of producing never. I think the never type is a clearer indication that something is amiss.

  8. added
    SuggestionAn idea for TypeScript
    and removed
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    on Jun 8, 2016
  9. ahejlsberg commented on Jun 8, 2016

    @ahejlsberg
    Member

    Reopening as suggestion. More examples of this feature in #9016.

  10. ahejlsberg commented on Jun 8, 2016

    @ahejlsberg
    Member

    I'm coming around to the view that we should produce intersection types in type guards for unrelated types. In particular, given that isA(obj) || isB(obj) narrows the type of obj to A | B in the true branch, it seems very reasonable that isA(obj) && isB(obj) should narrow to A & B.

    It's a pretty simple change. I will look at putting up a PR.

  11. locked and limited conversation to collaborators on Jun 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

SuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions