Description
Pre-reading
JSX in TypeScript refresher: #3203
Original issue: #4861
Announcement: https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html
Docs: https://facebook.github.io/react/docs/reusable-components.html#stateless-functions
Intro: https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.d6c0m2mhd
Source: https://github.com/facebook/react/blob/0ef5112e892b2350756400c1d047a2213002bd7d/src/renderers/shared/reconciler/ReactCompositeComponent.js#L155
Detecting Stateless Functional Components (SFCs)
SFCs are invoked with three arguments: props
, context
, and updater
. These are the same three arguments passed to component class constructors, so the parameter information is not useful in distinguishing SFCs from classes.
SFCs are invoked without new
whereas classes are invoked with new
. However, some older React components ignore the fact that they are invoked that way, and may exist with just call signatures, so call vs construct signatures are not useful for SFC detection either.
We currently require that the tag expression used in an <Element />
expression resolve to something with a call or construct signature whose return type is assignable to JSX.ElementClass
. React expects that the result of an invocation of an SFC is the same type that we call JSX.Element
. So we can use the return type of call signatures of three or fewer parameters to distinguish SFCs from classes.
Our previous JSX typing design tried to fall back to reasonable behavior when there was only minimal type information in the JSX
namespace. When both JSX.Element
and JSX.ElementClass
are empty, however, SFC detection becomes much harder. I propose that a component is only an SFC if it is assignable to JSX.Element
and not assignable to JSX.ElementClass
-- this preserves our good fallback behavior when there isn't a solid react.d.ts
present, without too much additional complexity.
The Element Attributes Type for SFCs
Determining the element attributes type for a SFC is simple: it's the type of the first parameter to the function, or the empty type if the function has zero parameters. However, keep reading.
Resolving the ref
and key
madness in props types
Current State
TypeScript with React has very annoying problems around the ref
and key
attributes.
Problem A: the ref
and key
attributes are visible when consuming a class, e.g. <Element key="foo" />
is always valid, but the key is never visible inside the class, e.g. this.props.key
is never defined in the class render
method. This is only slightly annoying as it's hard to use them accidentally in any meaningful fashion.
Problem B: More seriously, it's very easy as a component author to make a component whose props
type lacks the key
and ref
properties, even though those properties should always exist. Because these attributes are optional on JSX elements, we can't even enforce this restriction through the type system.
Problem C: Finally, SFCs do not have a ref
attribute (because there's no instance to point to). This is less serious than problems A and B, but is still a hole.
Problem D: The big one. It is valid (and common) to put a key
attribute on an SFC-based element, but SFCs are never going to bother defining key
themselves in their parameter type:
function Greeter({name = 'world'}) {
return <div>Hello, {name}!</div>;
}
let names = ['Alice', 'Bob'];
// Error, no property 'key' exists ... !?
let x = <div>{names.map((n, i) => <Greeter name={n} key={i.toString()} /></div>;
Proposal
The proposed solution is to add two new types to the JSX
namespace: IntrinsicAttributes
and IntrinsicClassAttributes
:
interface IntrinsicAttributes {
key?: string;
}
interface IntrinsicClassAttributes<T> {
ref?: string|((elem: T) => void);
}
When determining the element attributes type (the type that determines the allowed attributes of an element), we would intersect the existing type (as determined by the current algorithm) with the properties of either both interfaces (for intrinsic and class-based elements) or just IntrinsicAttributes
(for SFCs). IntrinsicClassAttributes<T>
would be instantiated with the class instance type for T
.
This solves problems A, B, C, and D: it becomes impossible to forget to define ref
and key
, it becomes an error to access ref
or key
from this.props
, and it becomes an error to attempt to define a ref
attribute when writing a JSX element with a SFC.
If desired, we could have only one IntrinsicAttributes
interface with both ref
and key
. This would slightly simplify the .d.ts and implementation at the expense of not solving problem C (it would be legal to write an incorrect ref
attribute on a SFC JSX element).
Decision Points
- What to do about SFC detection when
JSX
namespace isn't filled in:- Check for assignable to
Element
but not assignable toElementClass
- Ignore the problem and default to whatever behavior
- Something else
- Check for assignable to
- Props simplification: Choose one:
- Keep as-is
- Add
IntrinsicAttributes
- Add
IntrinsicAttributes
andIntrinsicClassAttributes
/cc @ahejlsberg @billti