DEV Community

Discussion on: Control flow analysis of aliased conditional expressions in TypeScript

Collapse
anthonyjoeseph profile image
Anthony G

Great article, thanks for writing it! I had a quick question:

Whilst an aliased conditional expression on a destructured field will allow for narrowing the original object's type, the flow analysis cannot narrow the type of a destructured sibling.

I'm not sure I understand this point. What do type predicates on foo do to the inferred type of bar in these cases?

function fn({ foo, bar }: Baz) { ... }
// vs
function fn(baz: Baz) {
  const { foo, bar } = baz
  ...
Enter fullscreen mode Exit fullscreen mode

Here's a playground of what I thought was supposed to happen but didn't

Collapse
willheslam profile image
Will Heslam Author

Thanks! :)

I definitely under explained this point... currently no type predicates on foo can act on the inferred type of bar.
In your example, bar will always be number | boolean, regardless.

However, type predicates on foo can influence the inferred type of baz, provided it's available in scope. Hence the differentiation between destructuring in the function signature rather than in the body: in the former there's no baz to influence.

I'm glad you raised this point because I realised I didn't know exactly how type narrowing works for object unions!

Your playground is a good example - I'd intuitively assume that the type of foo or bar could be used to differentiate which part of the union baz happens to be.
That actually doesn't work, which surprised me.

If you ignore type predicate/guard functions, it turns out TS has just two ways* to narrow unions of objects: in operator narrowing and discriminated unions.

So you either have to add an extra property to one of the unions, e.g.

if('qux' in baz){
  ...
Enter fullscreen mode Exit fullscreen mode

which defeats the point of destructuring, or by changing one of the fields to be a discriminant.

Discriminants must be a common property of the union where each type is a literal.
Here's a version of your playground where foo is now a discriminant, and baz is now influenced by a condition on foo.

There's three types of literal types: strings, numbers and booleans.

I wonder if it'd be useful if there was a prefix keyword to indicate which fields of an object should be discriminants, similar to readonly.
That way TS could indicate if you've accidentally broken the contract during refactoring - maybe it's overkill though.

Hope that answers your question, and thanks for raising it - I'll be more careful when creating object unions in the future. :)

* It wouldn't surprise me if there were some extra undocumented ways to narrow object unions!

Collapse
anthonyjoeseph profile image
Anthony G

Ah amazing! Your playground explains it really well. I guess my issue wasn't really related to your article 😅 - I hadn't realized that unions can only discriminate on literal types. Amazing that baz can be narrowed by foo - powerful stuff!