Typescript: Consider property access a form of type guards

Created on 25 Nov 2014  路  10Comments  路  Source: microsoft/TypeScript

This is a proposal for using property access as another form of type guards (see #900) to narrow union types. While we're investigating expanding the power of type guards (#1007) this feature would support the natural style that JavaScript programmers have in their code today.

Using property access to narrow union types

var x: string|number;
if (x.length) { // no error even though number lacks a length property
    var r = x.charAt(3); // x is of type string inside this block
}

var r2 = x.length || x * x; // no error, x narrowed to number on right hand side, r3 is number

var y: string[]|number[]|string;
if (y.length && y.push) {
    // y is string[]|number[] now
    var first = y[0]; // first is string|number
    if (first.length) {
        // first is string in here
    } else {
        // first is number in here
    }
}

We do not expand the situations in which types are narrowed, but we do expand the known type guard patterns to include basic property access. In these narrowing contexts it would be an error to access a property that does not exist in at least one of the constituent types of a union type (as it is today). However, it would now be valid to access a property that exists in at least one, but not all, constituent types. Any such property access will then narrow the type of the operand to only those constituent types in the union which do contain the accessed property. In any other context property access is unchanged.

Invalid property access

var x: string|number;

var r = x.length; // error, not a type guard, normal property access rules apply

if (x.len) { } // error, len does not exist on type string|number 

var r3 = !x.len && x.length; // error, len does not exist on type string|number 

var r4 = x.length && x.len; // error, len does not exist on type string

Issues/Questions

  • Should the language service behave differently in these type guard contexts? Should dotting off a union type in a type guard list all members on all types rather than only those that exist on all of them?
  • Need to understand performance implications

I have a sample implementation and tests in a branch here: https://github.com/Microsoft/TypeScript/tree/typeGuardsViaPropertyAccess. There're a couple bugs remaining but examples like the above all work and no existing behavior was changed.

Awaiting More Feedback Suggestion

Most helpful comment

Since this is tagged "Awaiting more feedback", I'll just link to https://github.com/microsoft/TypeScript/issues/36194 as a potential use case.

All 10 comments

:+1: for this.

For the existing issues/questions:

Should the language service behave differently in these type guard contexts? Should dotting off a union type in a type guard list all members on all types rather than only those that exist on all of them?

I assume that solely in these typeguard contexts, you're allowing a property access for properties that exist on any of the constituent types. If so, I'd think it makes sense to extend this sort of logic to the language service. After all, I assume we'll still give errors in the appropriate contexts. For instance,

interface A { bar: { bizzle: any } };
interface B { }
var foo: A | B;
if (foo.bar

is fine, but as soon as you have something like

var foo: A | B;
if (foo.bar.bizzle

the LS will complain that bar does not exist on type A | B, and hopefully that bizzle is a ridiculous property name. The biggest problem is that that's not a terribly helpful error message, which we could work on.

Need to understand performance implications

Shouldn't be terrible if user-defined type tags _supposedly_ won't affect perf either, but it'll be good to keep an eye on it.

Any update on this?

if i can weigh in:

Should the language service behave differently in these type guard contexts? Should dotting off a union type in a type guard list all members on all types rather than only those that exist on all of them?

In addition to what Daniel said, what would be cool is when getCompletion is called, intellisense shows an info/warning symbol near the properties that don't belong to the union type and maybe some text indicating that this property exists in only TypeA or TypeA | TypeB or TypeC etc...

Also, in the case of

var foo: A | B;
if (foo.bar.bizzle

if bar doesn't exist in the union type, maybe
if (foo.bar.bizzle)
could be emitted to
if (foo.bar && foo.bar.bizzle)
and that way, we could allow completion.

Since this is still open, it seems that the original example here is no longer valid?

if (x.length) { // no error even though number lacks a length property
    var r = x.charAt(3); // x is of type string inside this block
}

The code above now errors at x.length and seems this is the case per https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Advanced%20Types.md

Can anyone confirm?

The example was invalid at the time this was written as well. The idea was that at certain locations where you want to check a member on the type, you shouldn't need to cast, and that if certain types in a union lacked that member, they'd be "narrowed out".

+1

I hate noise +1'ing. Some repos seem to have a separate thumbs up icon you can click on under the original issue post and it will tally. Absent that, I'm indicating this is of value.

@rcollette There's an emoji smiley in the upper right hand corner of all comments - use that to add a :+1: (or other reaction), as you just described.

Ah. The you can click on it to add to it once its there. I've never been first I guess. (gee that reminds me of the days when you'd see "first" as the only response to a post).

I would love to see this implemented. User-defined type guard functions which are currently required in lieu of this proposal, litter the code with functions which would be unnecessary in vanilla javascript. These functions exist only to support the type system, yet they remain when compiled down to javascript.

Another benefit of implementing property access type guards is that user-defined type guards are often unsafe. For example:

interface Foo {
  a: string;
}

interface Bar {
  b: string;
}

function isFoo (o : Foo | Bar): o is Foo {
  return (<Foo> o).a !== undefined;
}

This works as intended. But if for some reason, later on Bar is expanded to:

interface Bar {
  b: string;
  a: string;
}

Now we have a bug that is uncaught by the type checker because the type guard essentially bypasses it with the type assertion. It would seem that type assertions are almost always present in user-defined type guards.

Since this is tagged "Awaiting more feedback", I'll just link to https://github.com/microsoft/TypeScript/issues/36194 as a potential use case.

Was this page helpful?
0 / 5 - 0 ratings