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
Merged

Conversation

IdeaHunter
Copy link
Contributor

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)

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ("a" in x) {
x.a = "1";
} else {
x.b = "1";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like it might do the trick

Copy link
Contributor Author

@IdeaHunter IdeaHunter Apr 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor Author

@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
Copy link
Member

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

@IdeaHunter
Copy link
Contributor Author

ping @RyanCavanaugh @ahejlsberg

@EliSnow
Copy link

EliSnow commented May 16, 2017

I'm really looking forward to this landing!

@EliSnow
Copy link

EliSnow commented Jun 7, 2017

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

@hax
Copy link

hax commented Sep 12, 2017

Can I ask what block this PR being merged?

@microsoft microsoft deleted a comment from msftclas Sep 27, 2017
@pelotom
Copy link

pelotom commented Nov 5, 2017

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

@IdeaHunter
Copy link
Contributor Author

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

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'.
Copy link

@Jessidhia Jessidhia Dec 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

@RyanCavanaugh RyanCavanaugh Dec 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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
Copy link
Contributor Author

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants