Repository: TypeScript
Issue: Proposal: stronger JSX types through conditional types
Labels: Suggestion,In Discussion

<!-- 🚨 STOP 🚨 𝗦𝗧𝗢𝗣 🚨 𝑺𝑻𝑶𝑷 🚨

Half of all issues filed here are duplicates, answered in the FAQ, or not appropriate for the bug tracker. Please read the FAQ first, especially the "Common Feature Requests" section.

-->

## Suggestion

The current way the `JSX` namespace works and is implemented in the compiler is... full of legacy stuff. This could probably be fixed by https://github.com/Microsoft/TypeScript/issues/14729, but even when that is made, we still need some way of dealing with the types of intrinsic attributes.

I'm not quite sure how to articulate my proposal, take this as a weak draft/WIP, but to sketch my idea, compare the following snippet with the way the `JSX` namespace [is currently defined in `@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8fbdf7311ae14dec1fef7d7969121328deced217/types/react/index.d.ts#L2655-L2855).

I wrote some tests kind of inline, and I named it "ESX" for now because I wrote it inside an existing project to verify the types worked.

```ts
// tests
const Test = (_: { test: boolean }) => false
const p: ESX.ComponentProps<typeof Test> = {
  test: true
}
const pFail: ESX.ComponentProps<typeof Test> = {
  test: true,
  children: null // $ExpectError
}

class Test2 extends React.Component<{ test: boolean }> {
  render() {
    return false
  }
}
const p2: ESX.ComponentProps<typeof Test2> = {
  test: true
}

declare const Suspense: ESX.ExoticComponents.ModeComponent<typeof ESX.ExoticComponents.Suspense>
const p3: ESX.ComponentProps<typeof Suspense> = {
  fallback: null
}

declare const Fragment: ESX.FragmentComponentType
const p4: ESX.ComponentProps<typeof Fragment> = {}

const aProps: ESX.ComponentProps<'a'> = {
  href: 'test',
  onClick({ currentTarget }) {
    currentTarget.href
  }
}

declare const MemoTest: ESX.ExoticComponents.MemoComponent<typeof Test>
const memoProps: ESX.ComponentProps<typeof MemoTest> = {
  test: true
}

function forwarder(props: ESX.ComponentProps<typeof Test>, ref: React.Ref<HTMLAnchorElement>) {
  const children: ESX.Element<typeof MemoTest> = {
    type: MemoTest,
    key: null,
    props: {
      test: true
    },
    ref: null
  }
  const element: ESX.Element<'a'> = {
    type: 'a',
    key: null,
    props: {
      children
    },
    ref
  }
  const fragment: ESX.Element<typeof Fragment> = {
    type: Fragment,
    key: null,
    props: {
      children: [element, 'foo']
    },
    ref: null
  }
  return fragment
}
declare const ForwardTest: ESX.ExoticComponents.ForwardComponent<typeof forwarder>
const forwardProps: ESX.ComponentProps<typeof ForwardTest> = {
  test: true
}
const forwardElement: ESX.Element<typeof ForwardTest> = {
  type: ForwardTest,
  key: 'foo',
  props: {
    test: true
  },
  ref(ref) {
    if (ref !== null) {
      ref.href
    }
  }
}

// actual declarations
declare global {
  namespace ESX {
    type EmptyElementResult = boolean | null
    type SingleElementResult<T extends Component = any> = string | number | Element<T>
    type FragmentResult<T extends Component = any> = EmptyElementResult | SingleElementResult<T> | FragmentResultArray<T>
    interface FragmentResultArray<T extends Component = any> extends ReadonlyArray<FragmentResult<T> | undefined> {}

    type Component =
      | ((props: any) => FragmentResult)
      | (new (props: any) => { render(): FragmentResult })
      | keyof typeof IntrinsicComponents
      | ExoticComponent
    type ExoticComponent =
      | ExoticComponents.ForwardComponent<any>
      | ExoticComponents.MemoComponent<any>
      | ExoticComponents.ModeComponent<any>

    const ChildrenPropName: 'children'
    type FragmentComponentType = ExoticComponents.ModeComponent<typeof ExoticComponents.Fragment>

    interface IntrinsicAttributes<T extends Component> {
      key?: string
      ref?: (ComponentRefType<T> extends never ? never : React.Ref<ComponentRefType<T>>) | null
    }

    type ApparentComponentProps<T extends Component> = IntrinsicAttributes<T> & ComponentProps<T>

    type ComponentProps<T extends Component> = T extends keyof typeof IntrinsicComponents
      ? IntrinsicComponentProps<T>
      : T extends (props: infer P) => FragmentResult
      ? P
      : T extends new (props: infer P) => { render(): FragmentResult }
      ? P
      : T extends ExoticComponents.ExoticComponentBase<ExoticComponentTypes>
      ? ExoticComponents.ExoticComponentProps<T>
      : never

    type IntrinsicComponentProps<
      T extends keyof typeof IntrinsicComponents
    > = typeof IntrinsicComponents[T] extends HostComponent<infer P, any> ? P : never

    type ComponentRefType<T extends Component> = T extends keyof typeof IntrinsicComponents
      ? IntrinsicComponentRef<T>
      : T extends ExoticComponents.ForwardComponent<infer C>
      ? C extends (props: any, ref: React.Ref<infer R>) => FragmentResult
        ? R
        : never
      : T extends (new (props: any) => infer R)
      ? R
      : never

    type IntrinsicComponentRef<
      T extends keyof typeof IntrinsicComponents
    > = typeof IntrinsicComponents[T] extends HostComponent<any, infer R> ? R : never

    interface Element<T extends Component> {
      type: T
      props: ComponentProps<T>
      key: string | null
      ref: React.Ref<ComponentRefType<T>> | null
    }

    type ExoticComponentTypes = typeof ExoticComponents[keyof typeof ExoticComponents]

    // these are non-callable, non-constructible components
    // the names inside them are to be used by the React types instead,
    // and are only here to be able to declare their props/refs to
    // the typechecker.
    namespace ExoticComponents {
      interface ExoticComponentBase<S extends ExoticComponentTypes> {
        $$typeof: S
      }

      const Memo: unique symbol
      const ForwardRef: unique symbol
      const Fragment: unique symbol
      const Suspense: unique symbol
      const ConcurrentMode: unique symbol
      const StrictMode: unique symbol

      interface ModeComponentProps {
        [ChildrenPropName]?: FragmentResult
      }
      interface SuspenseComponentProps extends ModeComponentProps {
        fallback: FragmentResult
        maxDuration?: number
      }

      // A bunch of this complication is that `type`s are
      // never allowed to be recursive, directly or indirectly
      type MemoComponentProps<T extends Component> = T extends HostComponent<infer P, any>
        ? P
        : T extends (props: infer P) => FragmentResult
        ? P
        : T extends new (props: infer P) => { render(): FragmentResult }
        ? P
        : T extends ForwardComponent<infer C>
        ? ForwardComponentProps<C>
        : never
      type ForwardComponentProps<T extends ForwardComponentRender> = T extends (
        props: infer P,
        ref: React.Ref<any>
      ) => FragmentResult
        ? P
        : never

      type ExoticComponentProps<
        T extends ExoticComponentBase<ExoticComponentTypes>
      > = T extends ExoticComponentBase<infer S>
        ? S extends typeof Fragment | typeof ConcurrentMode | typeof StrictMode
          ? ModeComponentProps
          : S extends typeof Suspense
          ? SuspenseComponentProps
          : S extends typeof Memo
          ? T extends MemoComponent<infer C>
            ? MemoComponentProps<C>
            : never
          : T extends ForwardComponent<infer C>
          ? ForwardComponentProps<C>
          : never
        : never

      interface ModeComponent<
        S extends typeof Fragment | typeof Suspense | typeof ConcurrentMode | typeof StrictMode
      > extends ExoticComponentBase<S> {}

      interface MemoComponent<T extends Component> extends ExoticComponentBase<typeof Memo> {
        type: T
      }

      type ForwardComponentRender = (props: any, ref: React.Ref<any>) => FragmentResult

      interface ForwardComponent<T extends ForwardComponentRender>
        extends ExoticComponentBase<typeof ForwardRef> {
        render: T
      }
    }
  }

  namespace IntrinsicComponents {
    const a: HostComponent<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>
    const div: HostComponent<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
  }

  const HostComponentBrand: unique symbol
  interface HostComponent<P, I> {
    [HostComponentBrand]: new (props: P) => I
  }
}
```


## Use Cases

Make the type definitions for JSX much stronger than they currently are. Using JSX syntax would, through the use of this new intrinsic type declaration style, be able to produce strongly typed elements, avoid the pitfalls of implicit `children`, and support actual exotic elements that are not callable or constructible.

This is, of course, not complete at all. I did say it is a draft but it is a starting point for further ideas. This doesn't address `defaultProps` at all for example.

## Examples

The examples are in the snippet above, but, when writing JSX:

```xml
<ComponentName key='x'>child text node<><div>fragment</div></></ComponentName>
```

The JSX evaluator would attempt to create `"ESX".Element` with `ComponentName`, `"ESX".FragmentComponentType` and `'div'` as their generic argument, respectively.

The attributes that can be given to the component would come from the `ApparentComponentProps<T>`. The attributes the component can read inside itself would be `ComponentProps<T>`. This no longer has any risk of having `key` or `ref` appear to be available as props inside a component, although I haven't yet found out a way to _forbid_ that you just declare `key` or `ref` yourself in your props; it'd be caught but only when you attempt to use the component, not on declaration time. This is likely related to the unsolved problem I mention at the end.

Children would count as a `["ESX".ChildrenPropName]` attribute. A TODO is to figure out how to represent the difference React and Preact have when dealing with single children. Right now, a single child (like inside `<>` and inside `<div>`) create a single `"ESX".Element`, while multiple children would create a `FragmentResultArray` (probably needs a better name).

Exotic components use "unique symbol" nominal types to be able to declare themselves. Intrinsic elements (`'div'`, `'a'`, etc) also use a namespace and `const` declarations instead of an `interface` as I was looking into using nominal typing for them as well. Host components use a "unique symbol" nominal type to make themselves not constructible while still being able to declare a component that behaves differently from class components, and are still not themselves exotic components.

An unsolved problem is how to declare that you expect an element or a component to have or at least accept certain props. This probably requires higher kinded types, or just an extra spark of the imagination to figure out how to do it. Using conditional types to `never` out an argument type if it doesn't accept the props you want is just terrible DX (`<T extends Component>`, `ComponentProps<T> extends { propIWant: string } ? T : never`).

I also ran into limitations with `type`s not being allowed to be self-referential. You probably want to avoid turning the type checker into a turing machine, but with (for example) `React.memo(React.lazy(async () => ({ default: React.memo(React.memo(React.memo(React.forwardRef(() => 'Hello there!')))) })))` being a [perfectly valid component at runtime](https://codesandbox.io/s/131q57nrqj), not being able to recurse causes issues for correctly deriving props. Reminds me I forgot to define the exotic lazy component type.

## Checklist

My suggestion meets these guidelines:

* [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  - Not really if you just use the React public API, but this is, however, breaking as hell for anything typed directly using the JSX types or `@types/react` non-concrete types. This would mostly affect `@types/react` itself, though, but several of `@types/react`'s types would have to change to be compatible with this.
* [x] This wouldn't change the runtime behavior of existing JavaScript code
* [x] This could be implemented without emitting different JS based on the types of the expressions
* [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
* [x] This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).