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

Add type guard for `in` keyword #15256

Merged
merged 4 commits into from Dec 6, 2017

Conversation

Projects
None yet
8 participants
@IdeaHunter
Copy link
Contributor

IdeaHunter commented Apr 18, 2017

Fixes #10485

Added support for typeguarding for following cases:

if ("a" in (x))
if ("a" in x)
if ("a" in x.prop)
if ("a" in x.outer.prop) 
if ('a' in this)
if ('a' in this.prop)

@msftclas msftclas added the cla-signed label Apr 18, 2017

function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
if ((type.flags & (TypeFlags.Union | TypeFlags.Object)) || (type.flags & TypeFlags.TypeParameter && (type as TypeParameter).isThisType)) {
const propName = literal.text;
return filterType(type, t => !!getPropertyOfType(t, propName) === assumeTrue);

This comment has been minimized.

Copy link
@RyanCavanaugh

RyanCavanaugh Apr 18, 2017

Member

Two spaces here... we should turn on a lint rule

This comment has been minimized.

Copy link
@IdeaHunter

IdeaHunter Apr 18, 2017

Author Contributor
if ("a" in x) {
x.a = "1";
} else {
x.b = "1";

This comment has been minimized.

Copy link
@RyanCavanaugh

RyanCavanaugh Apr 18, 2017

Member

It isn't correct to narrow to BOpn in this branch

This comment has been minimized.

Copy link
@IdeaHunter

IdeaHunter Apr 18, 2017

Author Contributor

@RyanCavanaugh it isn't clear what should we do here since "a" in {a:undefined} === true and how we should look on it from strictNullChecks perspective...
Can you please give an advice how we should narrow this guy?

This comment has been minimized.

Copy link
@IdeaHunter

IdeaHunter Apr 18, 2017

Author Contributor

Right now we have following logic: propertyIsInType === assumeTrue. If i got you right then we should change this logic to

propertyIsOptional 
            ? assumeTrue && propertyIsInType 
            : propertyIsInType === assumeTrue

right?

This comment has been minimized.

Copy link
@RyanCavanaugh

RyanCavanaugh Apr 19, 2017

Member

That seems like it might do the trick

This comment has been minimized.

Copy link
@IdeaHunter

IdeaHunter Apr 20, 2017

Author Contributor

Im changing behavior to requested one and now line 57 it gives us an error that seems make sense

tests/cases/compiler/inKeywordTypeguard.ts error TS2339: Property 'b' does not exist on type 'AWithOptionalProp | BWithOptionalProp'.
  Property 'b' does not exist on type 'AWithOptionalProp'
@IdeaHunter

This comment has been minimized.

Copy link
Contributor Author

IdeaHunter commented Apr 20, 2017

@RyanCavanaugh I have updated code to match requested behavior, can you, please, review it?
I would love to hear a feedback from you since i didn't even know do I moving in the right direction or not...

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Apr 24, 2017

Looks good overall to me. @ahejlsberg care to review?

@IdeaHunter

This comment has been minimized.

Copy link
Contributor Author

IdeaHunter commented May 1, 2017

@EliSnow

This comment has been minimized.

Copy link

EliSnow commented May 16, 2017

I'm really looking forward to this landing!

@EliSnow

This comment has been minimized.

Copy link

EliSnow commented Jun 7, 2017

Is it possible this will land for the Typescript 2.4 release?

@hax

This comment has been minimized.

Copy link

hax commented Sep 12, 2017

Can I ask what block this PR being merged?

@msftgits msftgits removed the cla-signed label Sep 26, 2017

@Microsoft Microsoft deleted a comment from msftclas Sep 27, 2017

@pelotom

This comment has been minimized.

Copy link

pelotom commented Nov 5, 2017

Can anyone comment as to why this PR is still languishing 6 months later?

@IdeaHunter

This comment has been minimized.

Copy link
Contributor Author

IdeaHunter commented Nov 5, 2017

There is two reasons of it:

  1. They don't care about it/don't like/don't have time for it/didn't yet got bitten in most precious place by a relevant bug 😆/have more important stuffs to do (reason doesn't really matter in this but the lack of motivation to spend some time on it is the only important thing, otherwise this PR would already be merged)
  2. Me not pushing it forward, because of using to babel+flow and didn't use typescript anywhere else(lack of motivation from my side to use typescript)
@@ -11336,6 +11336,34 @@ namespace ts {
return type;
}

function isTypePresencePossible(type: Type, propName: string, shouldHaveProperty: boolean) {
const prop = getPropertyOfType(type, propName);
if (!prop) {

This comment has been minimized.

Copy link
@RyanCavanaugh

RyanCavanaugh Dec 4, 2017

Member

Two edits in this body - rename shouldHaveProperty to assumeTrue and reduce the if to

if (prop) {
  return (prop.flags & SymbolFlags.Optional) ? true : assumeTrue;
}
return !assumeTrue;
@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Dec 4, 2017

Good to merge after one edit. Sorry for the delay on this 🙁

if ("a" in x) {
x.b = "1";
~
!!! error TS2339: Property 'b' does not exist on type 'A'.

This comment has been minimized.

Copy link
@Jessidhia

Jessidhia Dec 5, 2017

I'd really like to be able to use in for narrowing, but I'm not sure this is it...

Consider:

const foo = { a: true, b: '' }
negativeClassesTest(foo)

foo is only valid as a B, but it would pass the in check, and then be treated as an A; but its a key is not of type string.

This means that, within the branch, the type of x should still be A|B, but you are allowed to access .a; I just don't know what type .a should have. It probably should be any, which should trip --noImplicitAny if it's not used with a type narrowing expression.

Scenarios:

function negativeClassesTest(x: A | B) {
  if ("a" in x) {
    x.a // --noImplicitAny error
    const y: string = x.a // ideally a --noImplicitAny error, if it's possible to have "signalling any"s
    const z = x.a as string // accepted because it's a cast
    const u: string = typeof x.a === 'string' ? x.a : '' // accepted because it's narrowed
  }
}

It doesn't seem to be all that useful to work like this, but it at least allows you to access .a at all.

Bonus points, but probably hard to actually do in practice:

function negativeClassesTest(x: A | B) {
  if ("a" in x && typeof x.a === "string") {
    // x _should_ be an A!
  }
}

Alternative solution: Have a way to indicate that B has "not this key". Maybe, for example, interface B { a: never; b: string } should mean that B is not allowed to have an a key at all; even if it's set to a value of type never such as undefined!.

This comment has been minimized.

Copy link
@RyanCavanaugh

RyanCavanaugh Dec 5, 2017

Member

We considered the soundness aspect and it's not very different from existing soundness issues around aliased objects with undeclared properties. IOW you can already write this code, error-free (even in flow!), which observably fails:

interface T { x: string, y?: number };
const s = { x: "", y: "uh oh" };
const not_t: { x: string } = s;
const unsound: T = not_t;
(unsound.y && unsound.y.toFixed());

The reality is that most unions are already correctly disjointed and don't alias enough to manifest the problem. Someone writing an in test is not going to write a "better" check; all that really happens in practice is that people add type assertions or move the code to an equally-unsound user-defined type predicate. On net I don't think this is any worse than the status quo (and is better because it induces fewer user-defined type predicates, which are error-prone if written using in due to a lack of typo-checking).

For automatically disjointed unions, see #14094.

This comment has been minimized.

Copy link
@IdeaHunter

IdeaHunter Dec 6, 2017

Author Contributor

@Kovensky im agree, this is nasty case, but it grows from the fact we have structural type system in place. If we, for example, had nominal type system, the info we provided in function definitions would exactly matching all its usages and we never had this problem in place.

Workaround is to give compiler more complete info about types given, like this:

class A { a: string }
class B { b: string }
class C { a: number; b: string }
function z(x: A|B|C){
	if("a" in x){
		var num_a: number = x.a; // error TS2322: Type 'string | number' is not assignable to type 'number'.
                                         // Type 'string' is not assignable to type 'number'.
		var str_a: string = x.a; // error TS2322: Type 'string | number' is not assignable to type 'string'.
                                         // Type 'number' is not assignable to type 'string'.
	}
}
@IdeaHunter

This comment has been minimized.

Copy link
Contributor Author

IdeaHunter commented Dec 5, 2017

Going to to do requested fixes later, after my work day ends :-)

@IdeaHunter IdeaHunter force-pushed the IdeaHunter:in-typeguard branch from 8e79460 to 069f73d Dec 5, 2017

@RyanCavanaugh RyanCavanaugh merged commit c2fc5ea into Microsoft:master Dec 6, 2017

5 checks passed

TypeScript Test Run typescript_node.6 Build finished.
Details
TypeScript Test Run typescript_node.8 Build finished.
Details
TypeScript Test Run typescript_node.stable Build finished.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
license/cla All CLA requirements met.
Details

@Microsoft Microsoft locked and limited conversation to collaborators Jun 14, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can’t perform that action at this time.