Skip to content
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

Inferring Intersection Types #8911

Closed
leeavital opened this issue Jun 1, 2016 · 8 comments
Closed

Inferring Intersection Types #8911

leeavital opened this issue Jun 1, 2016 · 8 comments
Assignees
Labels
Suggestion An idea for TypeScript

Comments

@leeavital
Copy link

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.

@leeavital leeavital changed the title Inferring Union Types Inferring Intersection Types Jun 1, 2016
@kitsonk
Copy link
Contributor

kitsonk commented Jun 1, 2016

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;
  }
}

@leeavital
Copy link
Author

leeavital commented Jun 1, 2016

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?

@kitsonk
Copy link
Contributor

kitsonk commented Jun 1, 2016

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.

@leeavital
Copy link
Author

Why does it have to stop being an A ?

@weswigham
Copy link
Member

@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.

@ahejlsberg
Copy link
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.

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jun 7, 2016
@mhegazy mhegazy closed this as completed Jun 7, 2016
@ahejlsberg ahejlsberg added Suggestion An idea for TypeScript and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Jun 8, 2016
@ahejlsberg
Copy link
Member

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

@ahejlsberg ahejlsberg reopened this Jun 8, 2016
@ahejlsberg
Copy link
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.

@ahejlsberg ahejlsberg added this to the TypeScript 2.0 milestone Jun 8, 2016
@ahejlsberg ahejlsberg self-assigned this Jun 8, 2016
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants