title: Type narrowing with generic object type unions
body: <p>The following types are used in an event system for object changes.</p>
  
  <p>I'm having troubles with narrowing an object type based on certain conditions. For example, when the <code>prop</code> property is null, we know for sure that <code>deleted</code> will be false.</p>
  
  <p>The failing cases are demonstrated below: <a href="http://www.typescriptlang.org/play/#src=declare%20const%20change%3A%20Change%3C%7B%20a%3A%20number%3B%20b%3A%20string%20%7D%3E%0D%0A%0D%0Aif%20(change.prop%20%3D%3D%3D%20&#39;a&#39;)%20%7B%0D%0A%20%20%20%20change.prop%20%2F%2F%20OK%0D%0A%20%20%20%20change.newValue%20%2F%2F%20Expected%20to%20be%20%22number%22%0D%0A%7D%0D%0A%0D%0Aif%20(change.prop%20%3D%3D%20null)%20%7B%0D%0A%20%20%20%20change.prop%20%2F%2F%20Expected%20to%20be%20%22null%22%0D%0A%20%20%20%20change.deleted%20%2F%2F%20Expected%20to%20be%20%22false%22%0D%0A%20%20%20%20change.newValue%20%2F%2F%20Expected%20to%20be%20%22%7Ba%3A%20number%3B%20b%3A%20string%7D%22%0D%0A%7D%0D%0A%0D%0Atype%20Change%3CT%20%3D%20any%3E%20%3D%0D%0A%20%20%20%20%7C%20RootChange%3CT%3E%0D%0A%20%20%20%20%7C%20(T%20extends%20object%20%3F%20NestedChange%3CT%3E%20%3A%20never)%0D%0A%0D%0Atype%20RootChange%3CT%3E%20%3D%20IChange%20%26%20%7B%0D%0A%20%20%20%20prop%3A%20null%0D%0A%20%20%20%20oldValue%3A%20T%0D%0A%20%20%20%20newValue%3A%20T%0D%0A%20%20%20%20deleted%3A%20false%0D%0A%7D%0D%0A%0D%0Atype%20NestedChange%3CT%20extends%20object%20%3D%20any%2C%20P%20extends%20keyof%20T%20%3D%20keyof%20T%3E%20%3D%0D%0A%20%20%20%20%7C%20(IChange%20%26%20%7B%0D%0A%20%20%20%20%20%20%20%20prop%3A%20P%0D%0A%20%20%20%20%20%20%20%20oldValue%3A%20T%5BP%5D%0D%0A%20%20%20%20%20%20%20%20newValue%3A%20T%5BP%5D%0D%0A%20%20%20%20%20%20%20%20deleted%3A%20false%0D%0A%20%20%20%20%7D)%0D%0A%20%20%20%20%7C%20(IChange%20%26%20%7B%0D%0A%20%20%20%20%20%20%20%20prop%3A%20P%0D%0A%20%20%20%20%20%20%20%20oldValue%3A%20T%5BP%5D%0D%0A%20%20%20%20%20%20%20%20newValue%3A%20undefined%0D%0A%20%20%20%20%20%20%20%20deleted%3A%20true%0D%0A%20%20%20%20%7D)%0D%0A%0D%0Ainterface%20IChange%20%7B%0D%0A%20%20%20%20%2F**%20The%20property%20being%20changed.%20When%20null%2C%20this%20change%20is%20for%20the%20root%20value.%20*%2F%0D%0A%20%20%20%20prop%3A%20keyof%20any%20%7C%20null%0D%0A%20%20%20%20%2F**%20The%20previous%20value%20*%2F%0D%0A%20%20%20%20oldValue%3A%20unknown%0D%0A%20%20%20%20%2F**%20The%20next%20value%20*%2F%0D%0A%20%20%20%20newValue%3A%20unknown%0D%0A%20%20%20%20%2F**%20Whether%20the%20property%20has%20been%20deleted%20*%2F%0D%0A%20%20%20%20deleted%3A%20boolean%0D%0A%7D" rel="nofollow noreferrer">(playground link)</a></p>
  
  <pre><code>declare const change: Change&lt;{ a: number; b: string }&gt;
  
  if (change.prop === 'a') {
      change.prop // OK
      change.newValue // Expected to be "number"
  }
  
  if (change.prop == null) {
      change.prop // Expected to be "null"
      change.deleted // Expected to be "false"
      change.newValue // Expected to be "{a: number; b: string}"
  }
  
  type Change&lt;T = any&gt; =
      | RootChange&lt;T&gt;
      | (T extends object ? NestedChange&lt;T&gt; : never)
  
  type RootChange&lt;T&gt; = IChange &amp; {
      prop: null
      oldValue: T
      newValue: T
      deleted: false
  }
  
  type NestedChange&lt;T extends object = any, P extends keyof T = keyof T&gt; =
      | (IChange &amp; {
          prop: P
          oldValue: T[P]
          newValue: T[P]
          deleted: false
      })
      | (IChange &amp; {
          prop: P
          oldValue: T[P]
          newValue: undefined
          deleted: true
      })
  
  interface IChange {
      /** The property being changed. When null, this change is for the root value. */
      prop: keyof any | null
      /** The previous value */
      oldValue: unknown
      /** The next value */
      newValue: unknown
      /** Whether the property has been deleted */
      deleted: boolean
  }
  </code></pre>