title: TypeScript generic type predicate
body: <p>How do you write a generic type predicate in TypeScript?</p>
  
  <p>In the following example, <code>if (shape.kind == 'circle')</code> doesn't narrow the type to <code>Shape&lt;'circle'&gt;</code>/<code>Circle</code>/<code>{ kind: 'circle', radius: number }</code></p>
  
  <pre><code>interface Circle {
    kind: 'circle';
    radius: number;
  }
  
  interface Square {
    kind: 'square';
    size: number;
  }
  
  type Shape&lt;T = string&gt; = T extends 'circle' | 'square'
    ? Extract&lt;Circle | Square, { kind: T }&gt;
    : { kind: T };
  
  declare const shape: Shape;
  if (shape.kind == 'circle') shape.radius;
  // error TS2339: Property 'radius' does not exist on type '{ kind: string; }'.
  </code></pre>
  
  <p>I tried writing a generic type predicate to work around this, but the following doesn't work because <a href="https://github.com/Microsoft/TypeScript/wiki/FAQ#why-cant-i-write-typeof-t-new-t-or-instanceof-t-in-my-generic-function" rel="noreferrer">the type parameter isn't available at runtime</a></p>
  
  <pre><code>function isShape1&lt;T extends string&gt;(shape: Shape): shape is Shape&lt;T&gt; {
    return shape.kind extends T;
  }
  </code></pre>
  
  <p>The following does work, but only if the type parameter <code>T</code> is a literal (has the same value at compile- and runtime)</p>
  
  <pre><code>function isShape2&lt;T extends string&gt;(shape: Shape, kind: T): shape is Shape&lt;T&gt; {
    return shape.kind == kind;
  }
  
  if (isShape2(shape, 'circle')) shape.radius; // Works ✓
  
  declare const kind: string;
  if (!isShape2(shape, kind)) shape.kind;
  // error TS2339: Property 'kind' does not exist on type 'never'.
  </code></pre>
  
  <h2>Update 1</h2>
  
  <p>@jcalz The trouble is I need</p>
  
  <pre><code>declare const kind: string;
  if (kind != 'circle' &amp;&amp; kind != 'square') shape = { kind };
  </code></pre>
  
  <p>to work. I'd like to use a discriminated union, but can't, as you point out. If it were a discriminated union, could you write a generic type predicate?</p>
  
  <pre><code>type Shape&lt;T = string&gt; = Extract&lt;Circle | Square, { kind: T }&gt;;
  </code></pre>
  
  <p>The following still only works if the type parameter is a literal</p>
  
  <pre><code>function isShape3&lt;T extends Shape['kind']&gt;(shape: Shape, kind: T): shape is Shape&lt;T&gt; {
    return shape.kind == kind;
  }
  
  if (isShape3(shape, 'circle')) shape.radius; // Works ✓
  
  declare const kind: Shape['kind']; // 'circle' | 'square'
  if (!isShape3(shape, kind)) shape.kind;
  // error TS2339: Property 'kind' does not exist on type 'never'.
  </code></pre>
  
  <p>The only difference is in this case the compiler already provides a working type predicate</p>
  
  <pre><code>if (shape.kind != kind) shape.kind; // Works ✓
  </code></pre>
  
  <h2>Update 2</h2>
  
  <p>@jcalz At runtime could it for example do the same thing as <code>shape.kind == kind</code>?</p>
  
  <p>Here's a more concise demo</p>
  
  <pre><code>declare const s: string;
  declare const kind: 'circle' | 'square';
  declare let shape: 'circle' | 'square';
  
  if (s == kind) shape = s; // Works ✓
  if (shape != kind) shape.length; // Works ✓
  
  function isShape1(s: string, kind: 'circle' | 'square') {
    return s == kind;
  }
  
  if (isShape1(s, kind)) shape = s;
  // error TS2322: Type 'string' is not assignable to type '"square" | "circle"'.
  // https://github.com/microsoft/TypeScript/issues/16069
  
  function isShape2(
    s: string,
    kind: 'circle' | 'square'
  ): s is 'circle' | 'square' {
    return s == kind;
  }
  
  if (isShape2(s, kind)) shape = s; // Works ✓
  if (!isShape2(shape, kind)) shape.length;
  // error TS2339: Property 'length' does not exist on type 'never'.
  </code></pre>
  
  <h2>Update 3</h2>
  
  <p>Thanks @jcalz and @KRyan for your thoughtful answers! @jcalz's solution is promising, especially if I disallow the non-narrowing case, vs. merely disarming it (via overload).</p>
  
  <p>However it's still subject to the <a href="https://github.com/microsoft/TypeScript/issues/11670#issuecomment-261115991" rel="noreferrer">problem you point out</a> (Number.isInteger(), bad things happen). Consider the following example</p>
  
  <pre><code>function isTriangle&lt;
    T,
    K extends T extends K ? never : 'equilateral' | 'isosceles' | 'scalene'
  &gt;(triangle: T, kind: K): triangle is K &amp; T {
    return triangle == kind;
  }
  
  declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
  declare const kind: 'equilateral' | 'isosceles';
  
  if (!isTriangle(triangle, kind)) {
    switch (triangle) {
      case 'equilateral':
      // error TS2678: Type '"equilateral"' is not comparable to type '"scalene"'.
    }
  }
  </code></pre>
  
  <p><code>triangle</code> will never be narrower than <code>kind</code> so <code>!isTriangle(triangle, kind)</code> will never be <code>never</code>, thanks to the conditional type () however it remains narrower than it should be (unless <code>K</code> is a literal).</p>
  
  <h2>Update 4</h2>
  
  <p>Thanks again @jcalz and @KRyan for patiently explaining how this can in fact be accomplished, and the consequent weaknesses. I've chosen @KRyan's answer for contributing the fake-nominal idea, though your combined answers are extremely helpful!</p>
  
  <p>My takeaway is that the type of <code>s == kind</code> (or <code>triangle == kind</code> or <code>shape.kind == kind</code>) is built in and not (yet) available to users, to assign to other things (like predicates).</p>
  
  <p>I'm not sure that's exactly the same as <a href="https://github.com/microsoft/TypeScript/issues/15048" rel="noreferrer">one-sided type guards</a> b/c the false branch of <code>s == kind</code> does narrow in (one) case</p>
  
  <pre><code>declare const triangle: 'equilateral' | 'isosceles' | 'scalene';
  if (triangle != 'scalene')
    const isosceles: 'equilateral' | 'isosceles' = triangle;
  </code></pre>
  
  <p>And to better motivate this question in the first place</p>
  
  <ol>
  <li>I have a type which is almost a discriminated union (DNS RRs) except I can't enumerate all of the discriminant's values (in general it's a <code>string | number</code>, extensions are permitted). Consequently the built-in <code>rr.rdtype == 'RRSIG'</code> behavior doesn't apply. Unless I first narrow it to a true discriminated union with a user-defined type guard (<code>isTypedRR(rr) &amp;&amp; rr.rdtype == 'RRSIG'</code>), which isn't a terrible option.</li>
  <li>I can implement user-defined type guards for each RR type I can enumerate, but that's a lot of repetition (<code>function isRRSIG(rr): rr is RR&lt;'RRSIG'&gt;</code>, <code>function isDNSKEY(rr): rr is RR&lt;'DNSKEY'&gt;</code>, etc.). Probably this is what I'll continue to do: It's repetitious but obvious.</li>
  <li>The trouble with a trivial generic type guard is that non-literals aren't disallowed but don't make sense (unlike <code>s == kind</code>/<code>rr.rdtype == rdtype</code>). e.g. <code>function isRR&lt;T&gt;(rr, rdtype: T): rr is RR&lt;T&gt;</code>. Hence this question.</li>
  </ol>
  
  <p>This prevents me from say wrapping <code>isTypedRR(rr) &amp;&amp; rr.rdtype == rdtype</code> in <code>function isRR(rr, rdtype)</code>. Inside the predicate <code>rr</code> is narrowed rationally, but outside the only option is (currently) <code>rr is RR&lt;T&gt;</code> (or now a fake-nominal).</p>
  
  <p>Maybe when <a href="https://github.com/microsoft/TypeScript/issues/16069" rel="noreferrer">type guards are inferred</a>, it'll be trivial to rationally narrow the type outside the predicate as well? Or when <a href="https://github.com/microsoft/TypeScript/pull/29317" rel="noreferrer">types can be negated</a>, it'll be possible to make a true discriminated union given a non-enumerable discriminant. I do wish the type of <code>s == kind</code> were (more conveniently :-P) available to users. Thanks again!</p>