Donald Zhu
Donald Zhu

Reputation: 855

Typescript narrowing unable to parse out property type

I ran into this problem in my code:

interface Wide {
  prop: string | undefined
}

interface Narrow {
  prop: string
}

class Foo {
  prop: string
  constructor({ prop }: Narrow) {
    this.prop = prop
  }
}

const array = [{ prop: undefined }, { prop: 'foo' }]

array.forEach((obj: Wide) => {
  if (obj.prop && typeof obj.prop === 'string' && obj.prop !== undefined) {
    console.log(new Foo({ ...obj }))
    // Type 'undefined' is not assignable to type 'string'.
  }
})

Normally, I would think that Typescript would be able to deduce that if the if condition is passed, it means that the current obj that it is iterating on has a defined property prop with the type string. But I can't seem to get it to work.

Playground

Upvotes: 2

Views: 414

Answers (4)

p4m
p4m

Reputation: 376

Looks like the compiler can correctly narrow the type of obj.prop:

if (obj.prop && typeof obj.prop === 'string' && obj.prop !== undefined) {
  console.log(new Foo({ prop: obj.prop }))
}

But isn't smart enough to narrow down the type of the whole obj in this case. You can get it to work if you rewrite Wide to be a union:

type Wide = Narrow | {prop: undefined}

then the second variant will get filtered by the if statement.

Upvotes: 1

Alireza Ahmadi
Alireza Ahmadi

Reputation: 9893

That's because the obj is in Wide type even with string value (not undefined). So in this case you know more than ts compiler so you can use type assertion like this:

  if (obj.prop && typeof obj.prop === 'string' && obj.prop !== undefined) {
        let narrow:Narrow = obj as Narrow;
        console.log(new Foo(narrow))
    }
  }

PlayGroundLink

Upvotes: 1

aweebit
aweebit

Reputation: 646

You could fix it by providing a custom type predicate.

function isNarrow<T extends { prop: unknown }>(obj: T): obj is T & Narrow {
  return typeof obj.prop === 'string'
}

array.forEach((obj: Wide) => {
  if (obj.prop && isNarrow(obj)) {
    console.log(new Foo({ ...obj }))
  }
})

Note a few things:

  1. I do not think there is any need to copy the object using the spread syntax. You can just pass the original object to the constructor.
  2. I have kept the obj.prop part of the condition, but note that it does not accept the empty string. The obj.prop !== undefined part was not necessary because obj.prop cannot be undefined if its type is 'string'.

An alternative solution is to just use type assertions.

array.forEach((obj: Wide) => {
  if (obj.prop && typeof obj.prop === 'string') {
    console.log(new Foo({ ...obj } as Narrow))
  }
})

Upvotes: 1

strdr4605
strdr4605

Reputation: 4352

You need to use Type guard (type predicate)

interface Wide {
  prop: string | undefined
}

interface Narrow {
  prop: string
}

class Foo {
  prop: string
  constructor({ prop }: Narrow) {
    this.prop = prop
  }
}

const array = [{ prop: undefined }, { prop: 'foo' }]

function isNarrow(obj: Wide | Narrow): obj is Narrow {
  return typeof obj.prop === 'string';
}

array.forEach((obj: Wide) => {
  if (isNarrow(obj)) {
    console.log(new Foo({ ...obj }));
  }
})

Playground

Upvotes: 3

Related Questions