title: TypeScript type for React FunctionComponent that returns exactly one IntrinsicElement
body: <p>I want to create a TypeScript type that guarantees that a React <code>FunctionComponent</code> eventually renders exactly one HTML element.  That is, it can't be a <code>string</code>, a <code>number</code>, a <code>ReactFragment</code>, a <code>ReactPortal</code> (unless, I suppose, that portal returns exactly one HTML element), <code>null</code>, or a <code>boolean</code>.</p>
  <p>My understanding is that a type for all HTML elements is</p>
  <pre class="lang-ts prettyprint-override"><code>type IntrinsicElement = JSX.IntrinsicElements&lt;keyof JSX.IntrinsicElements&gt;;
  
  const anIntrinisicElement: IntrinsicElement = &lt;div /&gt;;
  
  // @ts-expect-error
  const notAnIntrinsicElement: IntrinsicElement = 'foo';
  </code></pre>
  <p>and indeed this compiles.</p>
  <p>So, I would expect the following to work for a <code>FunctionComponent</code> that returns exactly one <code>IntrinsicElement</code>:</p>
  <pre class="lang-ts prettyprint-override"><code>type IntrinsicFC&lt;P = unknown&gt; = FC&lt;P&gt; &amp; ((props: P) =&gt; IntrinsicElement);
  
  const SuccessFC: IntrinsicFC = () =&gt; &lt;div&gt;Goodbye&lt;/div&gt;;
  const FCWithProps: IntrinsicFC&lt;{ a: number }&gt; = (props: { a: number }) =&gt; 
    &lt;div&gt;{props.a}&lt;/div&gt;;
  
  // @ts-expect-error
  const FragmentFC: IntrinsicFC = () =&gt;
    &lt;&gt;
      &lt;div&gt;Hello&lt;/div&gt;
      foo
    &lt;/&gt;;
  
  // @ts-expect-error
  const NullFC = () =&gt; null;
  
  // @ts-expect-error
  const TextFC = () =&gt; 'foo';
  </code></pre>
  <p>Oddly, this does <em>not</em> compile.  None of the <code>// @ts-expect-error</code> components are actually faulty.</p>
  <p>If this <em>did</em> work, then I suspect my final type would be:</p>
  <pre class="lang-ts prettyprint-override"><code>type IntrinsicFC&lt;P = unknown&gt; = FC&lt;P&gt; &amp; {
    (props: P): IntrinsicElement | IntrinsicFC;
  };
  
  const NestedSuccessFC: IntrinsicFC = () =&gt; &lt;SuccessFC /&gt;;
  
  // @ts-expect-error
  const NestedFragmentFC: IntrinsicFC = () =&gt; &lt;FragmentFC /&gt;; // erroneously valid
  </code></pre>
  <p>So, my question is, why does <code>IntrinsicElement</code> reject <code>string</code>, <code>number</code>, and <code>null</code>, but <code>IntrinsicFC</code> not, and how can I fix it?</p>
  <p><strong>Update</strong>: I've discovered that <code>ReactFragment</code>s, for some reason, successfully compile as <code>IntrinsicElement</code>s.  This is probably because <code>ReactFragment</code> is defined as <code>{} | ReactNodeList</code>, and <code>{}</code> is just &quot;anything that isn't nullish&quot;.  On the other hand, <code>ReactFragment</code> does <em>not</em> extend <code>IntrinsicElement</code>, so I am not sure why it can be assigned.  That may or may not be the root of the issue.</p>
  <pre class="lang-ts prettyprint-override"><code>type IsIntrinsicElementAReactFragment =
    IntrinsicElement extends ReactFragment ? true : false; // true
  
  type IsReactFragmentAnIntrinsicElement = 
    ReactFragment extends IntrinsicElement ? true : false; // false
  
  // erroneously valid
  // @ts-expect-error
  const fragmentIsNotAnIntrinsicElement: IntrinsicElement = &lt;&gt;foo&lt;/&gt;;
  
  // correctly invalid
  // @ts-expect-error
  const notAnIntrinsicElement3: IntrinsicElement = &lt;TextFC /&gt;;
  </code></pre>
  <p><a href="https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDGMA0cDecBiAwtsujPlCgOYhIB28AvnAGZQQhwDkUqGXAbgBQQmAE8wSOAEkGUYHQDOwNAFEANkloM4AXjgApAMoANAHSyY8pSo1b6MRQG0A1kjEQWh0xbkLlaprajgC6wkJoEErwKHSW1sABdsEAXDJ+NoH2OvoAPAAmwABucAD0AHzhpaVwAAKOALRIAB6SGE1Q7FARUYrwfdZU0ooAchAwAIJxGUlBDmnx-rZzOdwsEBCCItV1jS1tMB1dPdFwdACu6urDY5PTVktZqekPmckOemeX6uHikjKKRZvFaTUgYCjUYKfIGzbLwFowej5RSIPjkSg0D4AfjgVnOUjSLBQ6kUSAEZRqeKQogkUmGYPRkIcUxhyzhnwZEMxOgRSJRrKe2NxUHxcEJxNJ5J2RJJ1KEO3qiiarSQ7SQnWgJz6rAxwRu4xZMzZzwF71WuXK6wguQqVUptJe1gCRFyAAVPuc6C46BAAO50cqfF2uwMAMjgAAoI2B2GBFGlXQBKPSB00gxO-B0C4Mer0+-2B-TBsO4IRwSMxiBxhOJhZGwU6AA+jseRGEjHCkVORnOaDQSEUiiIddezsInwjyd0gYKxXKAHENvkAEZiJA2wpFSpa+BEADqwBgAAtXbH4y3Mi68Cg0hcQMv1XBGIWK2e0tfb+d74-GFOZ5vyhwSs4zMFBGA3Oc7V2JV9lVQ51WOLttS5YJhwvMcJz-Ms4AtbDy1nLcAAkkCua1SgAvDWA2bCbW3eUakVZUDiOTUkPgEZviITCUy+K4oMY2C1Q1bo2LgAAVBEuP0SceK4K0th3OARgHRF8h7PsByHQgRydFQpMjP8cPU-tBy421tgYvYVSExDenYlSkHyFCHDQ7Nx2kwzcmchgzLo0SFFHY0HAAJh0x4zXgPJjM03z+Ks5iENYuyzgNe5dIbGBQvQoLzW88hx3M+joKYuCWJE5KfTuNM4QAZjC4F2TyCTmnyspKiAA" rel="nofollow noreferrer">TS Playground</a></p>