TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Decouple jsx element type from jsx factory return type and sfc return type

Open weswigham opened this issue 7 years ago • 115 comments

We need to look up the type of a jsx expression by actually resolving the jsx factory call, so that we don't create a reference to the global JSX.Element type, which can change shape between react versions (as it needs to in the react 16 upgrade). We also need to resolve the sfc return type and class element type from the parameters of the factory function overloads for the same reasons, doubly so because the types allowable as render method and SFC return values are no longer the same as JSX.Element (namely, they can be strings, arrays, portals, etc).

This might be considered a breaking change, because some consumers may expect JSX.Element to always be a supertype of both jsx element expression return types and SFC return types (even though this isn't true in react 16) - we certainly made that assumption internally, hence the need for the change. 🐱

weswigham avatar Feb 06 '18 21:02 weswigham

@weswigham would addressing this address https://github.com/Microsoft/TypeScript/issues/18357 ? It'd be great to be able to correctly type children render props in React.

aaronjensen avatar Feb 14 '18 02:02 aaronjensen

There are a lot of issues related to this that have been closed, saying that this issue will handle them. I am assuming that the case in #18357 will be handled by this as well.

At the core of this is the ability to type JSX based off of the createElement function, rather than using the set of interfaces in the JSX namespace. If that feature is indeed going to be implemented as part of this issue, then a natural extension would be that the children could end up anywhere in the resulting element, and they could be typed based off of anything, rather than just restricting children to the props.

jchitel avatar Feb 14 '18 20:02 jchitel

@jchitel agreed. I just hit this snag trying to change the react typings yesterday for this exact same reason.

ericanderson avatar Feb 21 '18 13:02 ericanderson

@weswigham If you haven't started on this yet, I'd like to take a crack at it

ericanderson avatar Feb 22 '18 01:02 ericanderson

@ericanderson I actually started work on this today, sorry 😉

weswigham avatar Feb 22 '18 01:02 weswigham

@weswigham Damn. Can you make sure I can capture the SFC or ComponentClass so we can do things like limit the children of <ButtonGroup> to <Button>?

ericanderson avatar Feb 22 '18 01:02 ericanderson

@ericanderson That's the plan. (I mean, technically it was a bug report a long time ago that we closed as "fixed" even though it was only half-fixed)

weswigham avatar Feb 22 '18 01:02 weswigham

I think that will also let us mark the defaultProps optional.

ericanderson avatar Feb 22 '18 01:02 ericanderson

By defaultProps do you mean the intrinsic props?

weswigham avatar Feb 22 '18 01:02 weswigham

I mean:

interface Props {
  foo: string,
  bar: number
}
class Foo extends React.Component<Props> {
  static defaultProps = { foo: "Hi Mom!" };
//...
}

In this world, the compiler should only require me to specify bar and foo should now be considered foo?: string

ericanderson avatar Feb 22 '18 01:02 ericanderson

... maybe. Even with localized types looked up from the signatuers, we still have to make assumptions about how the custom ctor/sfc is effectively built, because there's actually two calls going on. The first call is the constructor for the class or the SFC, (which are expected to take props as an argument), and can be overloaded - this is where a lot of props validation actually happens. The second call is the factory function itself, which actually needs the reified types from the inner call to be typechecked correctly (And today isn't actually typechecked at all, which is the root of most of these issues).

TBQH a lot of the JSX machinery in-place today is to get higher-ordery behavior from a system that used to not support any. I think now that we have conditional and infer types, there's a good chance I may actually be able to desugar a lot of the magic currently applied to JSX to normal typesystem operations. I'm generally just going to look at improving this as much as is feasible 😉

weswigham avatar Feb 22 '18 01:02 weswigham

So the props we should enforce are actually Omit<Props, typeof Foo.defaultProps> & Partial<typeof Foo.defaultProps>... which maybe we can't do with this change.

ericanderson avatar Feb 22 '18 01:02 ericanderson

@weswigham Thanks!

ericanderson avatar Feb 22 '18 01:02 ericanderson

I'm not entirely sure that this was technically fixed. #18131 was fixed, but we still don't actually look at what a given JSX factory invocation returns.

DanielRosenwasser avatar Mar 15 '18 06:03 DanielRosenwasser

Correct.

weswigham avatar Mar 15 '18 06:03 weswigham

What are the technical impediments to doing ReturnType<factoryFunction> in typechecking JSX?

...or do I have absolutely no idea how any of this stuff works.

dyst5422 avatar Mar 15 '18 18:03 dyst5422

ReturnType doesn't use overload resolution, nor does it apply type argument inference, both which utilize the actual props passed in to a JSX element.

So you'd potentially get an "under-instantiated" and/or broad type depending on if your JSX factory is generic or has overloads (or both).

DanielRosenwasser avatar Mar 15 '18 18:03 DanielRosenwasser

Oh yeah, now I remember reading that caveat to ReturnType<>. Bummer.

dyst5422 avatar Mar 15 '18 18:03 dyst5422

This improvement still won't allow recursively enforcing type constraints on JSX children, right? The use case I'm thinking of is React's new Context API.

// good
render() {
  let {Consumer, Provider} = React.createContext(undefined)
  return <Provider value={{a: 1}}>
    <Consumer>
      {state => state.a}
    </Consumer>
  </Provider>
}

// bad
render() {
  let {Consumer, Provider} = React.createContext(undefined)
  return <Consumer>
    {state =>
      <div>
        <Provider value={{a: 1}}>
          {state.a} // runtime error
        </Provider>
      </div>
    }
  </Consumer>
}

bcherny avatar Jun 11 '18 00:06 bcherny

This has been an issue for so long. When will we get a fix for this? My issue is similar to those described in the thread with typings children of ButtonGroup to only take Button. My issue is that i have RenderProp components that enhance elements however they break when passed a Component instance instead of a native JSX.Element and there is no way to type against it currently.

a Component instance I.E <ButtonGroup/> is inherently different to say a native jsx element <div/> they have different properties and different behaviors and we need a way to type them accordingly.

  • type on <ButtonGroup/> is a function type on native is a string.
  • refs and event bindings behave differently when passed to one or the other.
  • ReactDOM behaves differently when passed one or the other.
  • refs when applied behave differently consider ref.getBoundingClientRect(); being called on a ref of a component instance vs a native jsx element.

I heard rumors this would be fixed with typescript 2.8, and then 2.9 but still nothing. see comments for https://stackoverflow.com/questions/49269743/protect-against-react-instances with references to github.com/Microsoft/TypeScript/issues/21699

ShanonJackson avatar Jun 18 '18 01:06 ShanonJackson

Just to clarify, I understand that this issue is expected to allow us to define, for example, something like flowtype's https://flow.org/en/docs/react/children/#toc-only-allowing-a-specific-element-type-as-children

//… from flowtype's documentation
type Props = {
  children: React.ChildrenArray<React.Element<typeof TabBarIOSItem>>,
};

Am I right?

This feature could be a really big deal to use TypeScript in our Project (made with JS + React, looking for type-check it), because we have a lof of JSX components that we would like to type-check its children elements, and currently it is not posible to do with TypeScript.

SergioMorchon avatar Jul 23 '18 16:07 SergioMorchon

Yes, technically. You can already define such a type in TS, it just isn't terribly useful since every JSXExpression is erased to the base JSXElement.

weswigham avatar Jul 23 '18 16:07 weswigham

Related: https://github.com/Microsoft/TypeScript/issues/14789

dead-claudia avatar Aug 11 '18 03:08 dead-claudia

Does this fix the following problem?

let foo = React.createElement(Foo)
foo = <Foo />
/* [ts]
Type 'Element' is not assignable to type 'ComponentElement<{}, Foo>'.
  Types of property 'type' are incompatible.
    Type 'string | ComponentClass<any, any> | StatelessComponent<any>' is not assignable to type 'ComponentClass<{}, any>'.
      Type 'string' is not assignable to type 'ComponentClass<{}, any>'. */

If not, does the above problem have a fix in the works?

I expect <Foo /> to return a React.ComponentElement<Foo['props'], Foo> value, not a JSX.Element value. At least when I'm using "jsx": "react" in tsconfig.json.

Maybe I can work around this by editing/overriding the exports of @types/react?

aleclarson avatar Sep 20 '18 17:09 aleclarson

@aleclarson Yes, this would be the change which would hopefully allow that.

weswigham avatar Sep 20 '18 17:09 weswigham

@weswigham Awesome!

I actually started work on this today, sorry 😉

Is it still a high priority for you? 😎

edit: Oh, looks like it's in-progress still. Can't wait!

aleclarson avatar Sep 20 '18 18:09 aleclarson

Yeah, we got a lot of the way where with the new LibraryManagedAttributes entrypoint we added for react last release; but that just fixes up the props, the return type we have yet to manage.

weswigham avatar Sep 20 '18 18:09 weswigham

I'm yet another person who tried to fix the Functional Component types of react to be more open (and reflect what react can actually handle) only to be met with cryptic errors because of this issue ^^

this should compile indeed:

image

AlexGalays avatar Nov 15 '18 00:11 AlexGalays

Yeah, unfortunately right now when returning children you need to cast them to as JSX.Element | null 😢

Jessidhia avatar Nov 15 '18 01:11 Jessidhia

@Kovensky or self-recursive array, or string, or number, I believe, as of react 16.

ljharb avatar Nov 15 '18 02:11 ljharb