Open Jessidhia opened 5 years ago
This looks awesome, we might be able to perform a migration (if this goes through) simultaneously with having React's types declare a non-global JSX namespace.
I have one question -- wouldn't ApparentComponentProps
essentially be already solved with LibraryManagedAttributes
? As that is the differentiator between a component's "inner props" (accessible inside) vs what attributes are allowed in the JSX expression?
Can you also elaborate on use cases for this?
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 (
, ComponentProps extends { propIWant: string } ? T : never).
@ferdaber probably the easiest example is styled-components
, as it always only injects the same prop. Because it injects a className: string
when rendering the component it's wrapping, it would be good to be able to specify that the component accepts at least { className?: string }
.
Actually, I wonder if it would be possible to just do it anyway by modifying the Component
and ExoticComponent
aliases. I suspect that the reason it isn't possible right now is because of defaultProps
and propTypes
causing the type to be invariant instead of contravariant.
I think I got something that works, but it also required a bunch of code duplication because of having to try really hard to avoid TypeScript complaining about circular types.
There still are other things to solve (and even more TODOs) but right now I have to do some other work π
// 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
}
}
}
function hocFactory<T extends ESX.Component<{ className: string }>>(Component: T) {
return function Wrapper(props: ESX.ComponentProps<T>): ESX.Element<T> {
return {
type: Component,
key: null,
props: {
// not spreading these props here is a type error π
...props,
className: 'string'
},
ref: null
}
}
}
const DivHoc = hocFactory('div')
const DivHocElement: ESX.Element<typeof DivHoc> = {
type: DivHoc,
key: null,
props: {},
ref: null
}
declare const Memo2: ESX.ExoticComponents.MemoComponent<(props: { className: string }) => string>
const Memo2Hoc = hocFactory(Memo2)
const Memo2HocElement: ESX.Element<typeof Memo2Hoc> = {
type: Memo2Hoc,
key: null,
props: {
// TODO: find a way to make hocFactory be able to omit or optionalize the props it provides
className: 'foo'
},
ref: null
}
// $ExpectError
const ErrorForwardHoc = hocFactory(ForwardTest)
declare const Forward2: ESX.ExoticComponents.ForwardComponent<
ESX.ExoticComponents.ForwardComponentRender<{ className?: string }, any>
>
const Forward2Hoc = hocFactory(Forward2)
const Forward2HocElement: ESX.Element<typeof Forward2Hoc> = {
type: Forward2Hoc,
key: null,
props: {},
ref: null
}
// 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<P extends object = any> =
| ((props: P) => FragmentResult)
| (new (props: P) => { render(): FragmentResult })
| {
[K in keyof typeof IntrinsicComponents]: ComponentAcceptsProps<K, P>
}[keyof typeof IntrinsicComponents]
| ExoticComponent<P>
type ExoticComponent<P extends object = any> =
| ExoticComponents.ForwardComponent<ExoticComponents.ForwardComponentRender<P, any>>
| ExoticComponents.MemoComponentWithProps<P>
| ExoticComponents.ExoticComponentAcceptsProps<ExoticComponents.ModeComponent<any>, P>
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 ComponentAcceptsProps<
T extends Component,
P extends object
> = T extends keyof typeof IntrinsicComponents
? (P extends IntrinsicComponentProps<T> ? T : never)
: T extends (props: infer O) => FragmentResult
? (P extends O ? T : never)
: T extends new (props: infer O) => { render(): FragmentResult }
? (P extends O ? T : never)
: T extends ExoticComponents.ExoticComponentBase<ExoticComponentTypes>
? ExoticComponents.ExoticComponentAcceptsProps<T, P>
: 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<any, any>> = 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
type ExoticComponentAcceptsProps<
T extends ExoticComponentBase<ExoticComponentTypes>,
P extends object
> = never
interface ModeComponent<
S extends typeof Fragment | typeof Suspense | typeof ConcurrentMode | typeof StrictMode
> extends ExoticComponentBase<S> {}
interface MemoComponentWithProps<P extends object> extends MemoComponent<Component<P>> {}
interface MemoComponent<T extends Component> extends ExoticComponentBase<typeof Memo> {
type: T
}
type ForwardComponentRender<P extends object, R> = (
props: P,
ref: React.Ref<R>
) => FragmentResult
interface ForwardComponent<T extends ForwardComponentRender<any, any>>
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
}
}
I made some more progress but... why is this an implicit any? TS knows the contextual type of the function.
The same thing happens with IntrinsicAttributes
too, the ref
parameter is becoming an implicit any:
// tests
function createElement<T extends ESX.Component>(
Component: T,
props: ESX.ApparentComponentPropsWithoutChildren<T>,
...children: ESX.ChildrenToTupleType<ESX.ChildrenPropType<T>>
): ESX.Element<T>
function createElement<T extends ESX.Component>(
Component: T,
props: ESX.ApparentComponentProps<T>
): ESX.Element<T>
function createElement<T extends ESX.Component<any>>(
Component: T,
props: ESX.ApparentComponentProps<T>,
...children: any[]
): ESX.Element<T> {
const { key, ref, ...actualProps } = (props || {}) as ESX.IntrinsicAttributes<any>
return {
type: Component,
key: key || null,
props: (children.length > 0
? { ...actualProps, children: children.length === 1 ? children[0] : children }
: actualProps) as ESX.ComponentProps<T>,
ref: ref || null
}
}
const Test = (_: { test: boolean }) => false
const p: ESX.ComponentProps<typeof Test> = {
test: true
}
const pFail: ESX.ComponentProps<typeof Test> = {
test: true,
children: null // $ExpectError
}
createElement(Test, { test: true })
class Test2 extends React.Component<{ test: boolean }> {
render() {
return false
}
}
const p2: ESX.ComponentProps<typeof Test2> = {
test: true
}
createElement(Test2, { test: true })
declare const Suspense: ESX.ExoticComponents.ModeComponent<typeof ESX.ExoticComponents.Suspense>
const p3: ESX.ComponentProps<typeof Suspense> = {
fallback: null
}
createElement(Suspense, null) // $ExpectError
createElement(Suspense, {}) // $ExpectError
createElement(Suspense, { fallback: null })
createElement(Suspense, { fallback: null, children: 'test' })
createElement(Suspense, { fallback: null }, 'test')
declare const Fragment: ESX.FragmentComponentType
const p4: ESX.ComponentProps<typeof Fragment> = {}
createElement(Fragment, null)
createElement(Fragment, { key: 'foo' })
createElement(Fragment, null, 'test', createElement(Suspense, { fallback: null }))
createElement('a', {
href: 'test',
onClick({ currentTarget }) {
currentTarget.href
}
})
declare const MemoTest: ESX.ExoticComponents.MemoComponent<typeof Test>
const memoProps: ESX.ComponentProps<typeof MemoTest> = {
test: true
}
createElement(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
}
}
}
createElement(ForwardTest, {
key: 'foo',
test: true,
ref(ref) {
if (ref !== null) {
ref.href
}
}
})
function hocFactory<T extends ESX.Component<{ className: string }>>(Component: T) {
return function Wrapper(props: ESX.ComponentProps<T>): ESX.Element<T> {
return {
type: Component,
key: null,
props: {
// not spreading these props here is a type error π
...props,
className: 'string'
},
ref: null
}
}
}
const DivHoc = hocFactory('div')
const DivHocElement: ESX.Element<typeof DivHoc> = {
type: DivHoc,
key: null,
props: {},
ref: null
}
declare const Memo2: ESX.ExoticComponents.MemoComponent<(props: { className: string }) => string>
const Memo2Hoc = hocFactory(Memo2)
const Memo2HocElement: ESX.Element<typeof Memo2Hoc> = {
type: Memo2Hoc,
key: null,
props: {
// TODO: find a way to make hocFactory be able to omit or optionalize the props it provides
className: 'foo'
},
ref: null
}
// $ExpectError
const ErrorForwardHoc = hocFactory(ForwardTest)
declare const Forward2: ESX.ExoticComponents.ForwardComponent<
ESX.ExoticComponents.ForwardComponentRender<{ className?: string }, any>
>
const Forward2Hoc = hocFactory(Forward2)
const Forward2HocElement: ESX.Element<typeof Forward2Hoc> = {
type: Forward2Hoc,
key: null,
props: {},
ref: null
}
function NoChild() {
return null
}
function SingleChild(props: { children: ESX.SingleElementResult }) {
return null
}
function TupleChild(props: { children: [string, string] }) {
return true
}
function MultipleChild(props: { children: ESX.FragmentResult }) {
return false
}
function NonStandardChild(props: { children(): null }) {
return props.children()
}
function OptionalChild(props: { children?: ESX.SingleElementResult }) {
return null
}
type NoChildType = ESX.ChildrenPropType<typeof NoChild>
type SingleChildType = ESX.ChildrenPropType<typeof SingleChild>
type TupleChildType = ESX.ChildrenPropType<typeof TupleChild>
type MultipleChildType = ESX.ChildrenPropType<typeof MultipleChild>
type NonStandardChildType = ESX.ChildrenPropType<typeof NonStandardChild>
type OptionalChildType = ESX.ChildrenPropType<typeof OptionalChild>
type NoChildTuple = ESX.ChildrenToTupleType<NoChildType>
type SingleChildTuple = ESX.ChildrenToTupleType<SingleChildType>
type TupleChildTuple = ESX.ChildrenToTupleType<TupleChildType>
type MultipleChildTuple = ESX.ChildrenToTupleType<MultipleChildType>
type NonStandardTuple = ESX.ChildrenToTupleType<NonStandardChildType>
type OptionalTuple = ESX.ChildrenToTupleType<OptionalChildType>
// 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<P extends object = any> = OrdinaryComponent<P> | ExoticComponent<P>
type OrdinaryComponent<P extends object = any> =
| ((props: P) => FragmentResult)
| (new (props: P) => { render(): FragmentResult })
| {
[K in keyof typeof IntrinsicComponents]: OrdinaryComponentAcceptsProps<K, P>
}[keyof typeof IntrinsicComponents]
type ExoticComponent<P extends object = any> = ExoticComponents.ExoticComponentAcceptsProps<
ExoticComponentSymbols,
P
>
const ChildrenPropName: 'children'
type FragmentComponentType = ExoticComponents.ModeComponent<typeof ExoticComponents.Fragment>
type ChildrenPropType<T extends Component> = 'children' extends keyof ComponentProps<T>
? ComponentProps<T> extends {
[ChildrenPropName]?: infer C
}
? C
: never
: never
type ChildrenToTupleType<T> =
| (// if T is itself never, make a 0-tuple
Extract<T, any> extends never
? []
: Exclude<T, ReadonlyArray<any>> extends never // ignore making a 1-tuple if there are no non-array elements
? never // make a 1-tuple which accepts the non-array children
: [Exclude<T, ReadonlyArray<any>>])
// if children is optional then make an empty tuple and also a 1-tuple with undefined
| (undefined extends T ? [] | [undefined] : never)
| (// if T is already an Array type (includes tuple types) we can preserve it
T extends Array<any>
? T // if it is a ReadonlyArray (like FragmentResultArray), convert to an array
: (T extends ReadonlyArray<infer C> ? C[] : never))
interface IntrinsicAttributes<T extends Component> {
key?: string
ref?: (ComponentRefType<T> extends never ? never : React.Ref<ComponentRefType<T>>) | null
}
type ApparentComponentProps<T extends Component> =
// TODO: skip the Pick if there is no keyof IntrinsicAttributes inside the ComponentProps
| Pick<ComponentProps<T>, Exclude<keyof ComponentProps<T>, keyof IntrinsicAttributes<any>>> &
IntrinsicAttributes<T>
// also accept null as props if _all props_ are optional
| ({} extends ComponentProps<T> & IntrinsicAttributes<T> ? null : never)
type ApparentComponentPropsWithoutChildrenIntermediate<
T extends Component
> = typeof ChildrenPropName extends keyof ComponentProps<T>
? Pick<ComponentProps<T>, Exclude<keyof ComponentProps<T>, typeof ChildrenPropName>>
: ComponentProps<T>
type ApparentComponentPropsWithoutChildren<T extends Component> =
// TODO: skip the Pick if there is no keyof IntrinsicAttributes inside the ComponentProps
| Pick<
ApparentComponentPropsWithoutChildrenIntermediate<T>,
Exclude<
keyof ApparentComponentPropsWithoutChildrenIntermediate<T>,
keyof IntrinsicAttributes<any>
>
> &
IntrinsicAttributes<T>
// also accept null as props if _all props_ are optional
| ({} extends ApparentComponentPropsWithoutChildrenIntermediate<T> & IntrinsicAttributes<T>
? null
: never)
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 ExoticComponentTypes
? ExoticComponents.ExoticComponentProps<T>
: never
type OrdinaryComponentAcceptsProps<
T extends OrdinaryComponent,
P extends object
> = T extends keyof typeof IntrinsicComponents
? (P extends IntrinsicComponentProps<T> ? T : never)
: T extends (props: infer O) => FragmentResult
? (P extends O ? T : never)
: T extends new (props: infer O) => { render(): FragmentResult }
? (P extends O ? T : never)
: never
type ComponentAcceptsProps<T extends Component, P extends object> = T extends OrdinaryComponent
? OrdinaryComponentAcceptsProps<T, P>
: ExoticComponents.ExoticComponentAcceptsProps<T, P>
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 ExoticComponentSymbols = typeof ExoticComponents[keyof typeof ExoticComponents]
type ExoticComponentTypes = {
[S in ExoticComponentSymbols]: ExoticComponents.ExoticComponent<S, any>
}[ExoticComponentSymbols]
// 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 ExoticComponentSymbols> {
$$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
type ExoticComponent<
S extends ExoticComponentSymbols,
T extends S extends typeof Memo
? Component
: S extends typeof ForwardRef
? ForwardComponentRender<any, any>
: any
> = S extends typeof Fragment | typeof ConcurrentMode | typeof StrictMode | typeof Suspense
? ModeComponent<S>
: S extends typeof Memo
? MemoComponent<T>
: S extends typeof ForwardRef
? ForwardComponent<T>
: never
type ExoticComponentAcceptsProps<SS extends ExoticComponentSymbols, P extends object> = {
[S in SS]: S extends typeof Fragment | typeof ConcurrentMode | typeof StrictMode
? P extends ModeComponentProps
? ModeComponent<S>
: never
: S extends typeof Suspense
? P extends SuspenseComponentProps
? ModeComponent<S>
: never
: S extends typeof Memo
? MemoComponentWithProps<P>
: S extends typeof ForwardRef
? ForwardComponentWithProps<P>
: never
}[SS]
interface ModeComponentProps {
[ChildrenPropName]?: FragmentResult
}
interface SuspenseComponentProps extends ModeComponentProps {
fallback: FragmentResult
maxDuration?: number
}
type ExoticComponentProps<T extends 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
// 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<any, any>> = T extends (
props: infer P,
ref: React.Ref<any>
) => FragmentResult
? P
: never
interface ModeComponent<
S extends typeof Fragment | typeof Suspense | typeof ConcurrentMode | typeof StrictMode
> extends ExoticComponentBase<S> {}
interface MemoComponentWithProps<P extends object> extends MemoComponent<Component<P>> {}
interface MemoComponent<T extends Component> extends ExoticComponentBase<typeof Memo> {
type: T
}
interface ForwardComponentWithProps<P extends object>
extends ForwardComponent<ForwardComponentRender<P, any>> {}
type ForwardComponentRender<P extends object, R> = (
props: P,
ref: React.Ref<R>
) => FragmentResult
interface ForwardComponent<T extends ForwardComponentRender<any, any>>
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
}
}
Have you gotten parametric conditional types to work with checking against never
? The last time I tried it always short circuits:
type IsNever<T> = T extends never ? true : false
type foo = IsNever<boolean> // false
type bar = IsNever<never> // never <-- shortcircuit
Asking because of this line:
type ChildrenToTupleType<T> =
| (// if T is itself never, make a 0-tuple
Extract<T, any> extends never
? []
: Exclude<T, ReadonlyArray<any>> extends never // ignore making a 1-tuple if there are no non-array elements
? never // make a 1-tuple which accepts the non-array children
: [Exclude<T, ReadonlyArray<any>>])
That's what I'm doing with the Extract<T, any>
. The extra indirection seems to avoid the short circuiting.
Ah cool, I need some more time to grok this, I only wish I can leave inline comments on your comments π
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
.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.
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:
The JSX evaluator would attempt to create
"ESX".Element
withComponentName
,"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 beComponentProps<T>
. This no longer has any risk of havingkey
orref
appear to be available as props inside a component, although I haven't yet found out a way to forbid that you just declarekey
orref
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 aFragmentResultArray
(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 andconst
declarations instead of aninterface
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, 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:
@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.