TypeScript: typeof union.membername errors.

TypeScript Version: 2.8.0-dev.20180308

Search Terms: typeof union parameter undefined.

Code

declare var value: string | Date;
if (typeof value.now /*error!*/ !== "undefined") {}

Expected behavior: No error and appropriate type narrowing.

Actual behavior:

[ts]
Property 'now' does not exist on type 'string | Date'.
  Property 'now' does not exist on type 'string'.

Playground Link

NOTE I don’t like having to make a new function to properly narrow types for every union I create, is there another way around this (like manually overriding the variable’s type within the scope of a code block without declaring a new variable?).

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 1
  • Comments: 20 (12 by maintainers)

Most upvoted comments

You can use if ("queryname" in matchingoptions) { which narrows and doesn’t error. One advantage of using it in a union position is that if you misspelll the left operand, the right operand isn’t narrowed (which hopefully clues you in).

@Griffork No? The point is that when you ask for the type of value.now when value is type string | typeof Date, since now only exists on one union member, nothing is constraining its’ type on the other member, meaning you could have potentially assigned a value of a conflicting type to it in the past. For example,

type Union = {kind: "a", Foo: number} | {kind: "b", Foo: string} | {kind: "c"};
let a: Union;
function getVal() {
  return { kind: "c" as "c", Foo: true }
}
a = Math.random() > 0.5 ? getVal() : { kind: "a", Foo: 12 }
if (typeof a.Foo !== "undefined") {
  // a.Foo exists, yes. What is a.Foo's type here? No idea. Can't say it's `number | string`  -
  // we've clearly just constructed an expression above where it might not be! What's
  // `a`'s type? Don't know. Can't draw any conclusions on the type of `a` based on the
  // presence of `Foo`, since not all union members have a known value for it.
}

So while property access won’t immediately throw, it is not safe to use (and using it would likely be a code smell), since its type is indeterminate!

You should probably just cast inside your typeof. Alternatively, augment the String interface with an explicit property stating the type of the now member (undefined), to make the fields mutually exclusive (and prevent you from ever assigning something with a defined now type to a string):

interface String {
  now: undefined;
}
declare var value: string | typeof Date;
if (typeof value.now !== "undefined") {
  value.now();
}

I definitely don’t think this is bug (it’s a feature request) because this is definitely how union type property accesses are intended to work (properties only exist if they’re in all union members, and that’s how they’ve worked forever)… however allowing accesses of nonexistant union member properties for the purposes of checking for their existence seems reasonableish? Only -ish, though… I think union excess property checking will catch most of the obvious problems that could have cropped up from allowing it nowadays, so… it may be fine? I can’t convince myself it definitely is… Like, here’s the deal, if I say I have type Union = {kind: "a", Foo: number} | {kind: "b", Foo: string} | {kind: "c"}, and a variable x of type Union, I cannot confidently say that x.Foo is string | number | undefined, because someone could have used {kind: "c", Foo: true} as a perfectly valid value for that variable; all I can say is any, really, which doesn’t help. Unions aren’t actually closed to subtypes, which muddles this - discrimination is only possible for discriminable unions precisely because a field exists on all union members and whose types give that field a finite domain of possible values into which each possible union member can be bucketed (which is also why we only discriminate on unit types).

I think this is a bug. I guess the rule is that for typeof expr.id, if expr is a union type and id exists on at least one member, permit the check and narrow based on the property.

@nomcopter, in #22094 you did something similar.

Thanks! I agree the docs need some love.

We could have at most one consistency.

The in operator is used for all kinds of dynamic tests and we never had a rule in place to disallow any left operand of it, so we blessed it with the narrowing behavior because why not. There’s currently no special casing for property access anywhere.