If you're looking for a solid TypeScript JSX tutorial this is a great resource.
Last night I wanted to play with TypeScripts new support for JSX. In this post I'll walk through my process, and what I learned along the way. Hopefully you find this useful.
While it doesn't yet exist in the current version (at the time of this writing TypeScript is at 1.5), you can however grab a copy of the nightly builds from npm.
Get the most recent nightly build.
npm install -g typescript@next
The rest of this post was run against nightly build Version 1.6.0-dev.20150814
.
Given this sample React/JSX
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.render(<HelloMessage name="John" />, mountNode);
Create a TypeScript version of a JSX file.
Just like how TypeScript doesn't read .js
but looks for .ts
files (unless you hack it). TypeScript doesn't read .jsx
files. It instead looks for .tsx
files.
So if you save the above sample as a helloWorld.tsx
, we can then run the tsc
compiler against our helloWorld.tsx
file.
If I run tsc helloWorld.tsx
I get the following errors:
> tsc helloWorld.tsx
helloWorld.tsx(1,20): error TS2304: Cannot find name 'React'.
helloWorld.tsx(3,12): error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
helloWorld.tsx(7,1): error TS2304: Cannot find name 'React'.
helloWorld.tsx(7,14): error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
helloWorld.tsx(7,44): error TS2304: Cannot find name 'mountNode'.
Working through the errors...
I could just show you the final output that compiles, but want to include my learning process (stumbling) as I fumble through and figure out the new command.
Fixing error TS2304: Cannot find name 'React'.
If you've been using TypeScript for any amount of time, the first error should be easy to see. The compiler knows nothing about this thing called React
. And I haven't used React with TypeScript before. I don't want to go write a bunch of TypeScript type definitions for react and can easily pull down ones created already by using tsd
to install the Definitely Typed definitions for React
.
What is tsd?
If you haven't seen TSD before it's a great package manger utility for TypeScript Type Definitions.
It can be easily installed with npm install -g tsd
.
Installing React Type Definitions
UPDATE: Originally below I used tsd
to install react
but if you check out the comments react-global
works out better and you can avoid some of the hacks I put in place to compile React below.
tsd install react
which tsd
will download from Definitely Typed the react.d.ts
and place it in ./typings/react/react.d.ts
.
I then reference the file in our helloWorld.tsx
which gives me the following:
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.render(<HelloMessage name="John" />, mountNode);
When I re-run tsc helloWorld.tsx
hoping to get rid of the first error: hmmm
> tsc helloWorld.tsx
helloWorld.tsx(3,20): error TS2304: Cannot find name 'React'.
helloWorld.tsx(5,12): error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
helloWorld.tsx(9,1): error TS2304: Cannot find name 'React'.
helloWorld.tsx(9,14): error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
helloWorld.tsx(9,44): error TS2304: Cannot find name 'mountNode'.
Well, that didn't get rid of our error TS2304: Cannot find name 'React'.
. This threw me for a bit but eventually figured out that you need set it up by adding import React = __React;
.
So that gives us this:
import React = __React;
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.render(<HelloMessage name="John" />, mountNode);
Now we should see some errors going away. And we do...
> tsc helloWorld.tsx
helloWorld.tsx(6,12): error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
helloWorld.tsx(10,14): error TS2607: JSX element class does not support attributes because it does not have a 'props' property
helloWorld.tsx(10,14): error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
helloWorld.tsx(10,44): error TS2304: Cannot find name 'mountNode'.
Fixing error TS17004: Cannot use JSX unless the '--jsx' flag is provided.
The next error is new to me, but it makes some sense, so I add the --jsx
flag to tsc
and try tsc --jsx helloWorld.tsx
, but looks like I missed a parameter to --jsx
.
> tsc --jsx helloWorld.tsx
message TS6081: Argument for '--jsx' must be 'preserve' or 'react'.
In the current iteration of TypeScript 1.6 appears to have two options for --jsx
, both preserve
or react
.
preserve
will keep the jsx
in the output. I presume this is so you can use tools like JSX
to actually provide the translation.
react
will remove the jsx
syntax and turn it in to plain javascript so <div></div>
in the TSX file would become React.createElement("div", null)
.
By passing the react
option, here's where we end up:
> tsc --jsx react helloWorld.tsx
helloWorld.tsx(11,14): error TS2607: JSX element class does not support attributes because it does not have a 'props' property
helloWorld.tsx(11,44): error TS2304: Cannot find name 'mountNode'.
I'm going to tackle the last error next, as initially I didn't understand the JSX error above.
Fixing error TS2304: Cannot find name 'mountNode'.
This one I'll just make the compiler happy and presume we defined mountNode
as an html element probably a <div id="mountNode"></div>
somewhere in the global scope to keep this example short.
I place declare var mountNode: any;
near the top of my helloWorld.tsx
file and we're left with one last error:
> tsc
helloWorld.tsx(10,14): error TS2607: JSX element class does not support attributes because it does not have a 'props' property
Fixing error TS2607: JSX element class does not support attributes because it does not have a 'props' property
This last error is actually the one that had me stumped, and mostly why I'm writing this lengthy post so I hope you find it and can work through it a little quicker than it took me.
What's happening here is TypeScript is doing what it was intended to do. It's statically checking our JSX in this case.
If you look at our sample above where we call React.createClass(...)
and compare that to the type definition we see: function createClass<P, S>(spec: ComponentSpec<P, S>): ClassicComponentClass<P>;
you may notice P
and S
generic parameters to createClass<P, S>
which I didn't supply earlier.
The naming here wasn't immediately obvious, but some snooping around in the type definitions and I eventually found out P
is referring to the type we pass in defining the structure of the react props
and S
defines the state
.
So in this Hello World example when we placed name="John"
attribute inside the <HelloMessage name="John" />
element and since we didn't give a P
or S
to the React.createClass<P,S>(...)
, TypeScript was providing static type checking against an unknown type for P & S
. In this case saying that we can't apply the attributes to the element because we did not provide a generic type P
to define what props
are allowed to be included.
To fix this I create a type by using an interface
like below:
interface HelloWorldProps {
name: string;
}
When I call React.createClass
I pass in the HelloWorldProps
interface for the props
(P
) and an any
for the state (S
) like so: React.createClass<HelloWorldProps, any>(...)
YAY IT COMPILES!!!
Compiling the below by using tsc --jsx react helloWorld.tsx
import React = __React;
declare var mountNode: any;
interface HelloWorldProps {
name: string;
}
var HelloMessage = React.createClass<HelloWorldProps, any>({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.render(<HelloMessage name="John" />, mountNode);
We get the following output in helloWorld.js
var React = __React;
var HelloMessage = React.createClass({
render: function () {
return React.createElement("div", null, "Hello ", this.props.name, React.createElement("div", null));
}
});
React.render(React.createElement(HelloMessage, {"name": "John"}), mountNode);
Let's improve it now...
Since we started with a plain JavaScript version of our sample and we're now using TypeScript we get to take advantage of some of what TypeScript brings to the table.
But before we do this, let's first break our code (from a compiler) perspective to see what the above gave us...
Let's break our example on purpose to see how TypeScript responds?
I changed one character in two places in the working helloWorld.tsx
file and when I run the compiler I get the following two errors. Can you spot what changed?
import React = __React;
declare var mountNode: any;
interface HelloWorldProps {
Name: string;
}
var HelloMessage = React.createClass<HelloWorldProps, any>({
render: function() {
return <div>Hello {this.props.mane} <div></div></div>;
}
});
React.render(<HelloMessage name="John" />, mountNode);
Output:
> tsc --jsx react helloWorld.tsx
helloWorld.tsx(15,14): error TS2324: Property 'Name' is missing in type 'HelloWorldProps'.
helloWorld.tsx(15,28): error TS2339: Property 'name' does not exist on type 'HelloWorldProps'.
Did you spot the change made? If you did, amazing. If you didn't, don't feel bad - it's a very simple and easy error to make when writing plain javascript. One that can't be found without actually executing plain JS, debugging, running unit tests or other checkers before even finding the error.
If you didn't spot the change by looking directly at the code, can you spot the change by reading the compiler error output?
Ok, give up? - I changed the name
to Name
in the HelloWorldProps
interface definition AND in the JSX this.props.mane
I spelled the props name mane
wrong (should be Name
according to our interface definition). So then why did we only get the error on line 15 (that's the React.render(...)
line).
TypeScript in this case is using the HelloWorldProps
interface and it's definition to type-check the attributes used in the JSX <HelloMessage />
.
This is great, the compiler found the error right in the JSX before we even tried to execute the code.
Why didn't it detect the mane
mis-spelled variable?
I'm going to just take a guess on this one but I'm not a React guy yet, so it may have something to do with react internals (that I'm not feeling like digging into at the moment).
If you look at the react.d.ts
you'll see that React.createClass<P,S>()
returns a type of ClassicComponentClass<P>
.
Thanks to a tip from Ryan (ya, the famous Ryan from the TypeScript team) has a great write up about TypeScript and JSX we should be avoiding all of the above use of React.createClass(...)
and instead using the ES6
extends
functionality which we can leverage in TypeScript.
Let's re-write...
Turning the HelloMessage
variable into an ES6 class
we now also get type checking inside the component on this.props
options. YAY!!!:
import React = __React;
declare var mountNode: any;
interface HelloWorldProps extends React.Props<any> {
Name: string;
}
class HelloMessage extends React.Component<HelloWorldProps, {}> {
render() {
return <div>Hello {this.props.mane}</div>;
}
}
React.render(<HelloMessage name="John" />, mountNode);
The above gives us the following errors:
> tsc
helloWorld.tsx(11,35): error TS2339: Property 'mane' does not exist on type 'HelloWorldProps'.
helloWorld.tsx(15,14): error TS2324: Property 'Name' is missing in type 'HelloWorldProps'.
helloWorld.tsx(15,28): error TS2339: Property 'name' does not exist on type 'HelloWorldProps'.
Final Answer
So, a bit long winded, but below is the final sample HelloWorld React TypeScript JSX prototype.
import React = __React;
declare var mountNode: any;
interface HelloWorldProps extends React.Props<any> {
name: string;
}
class HelloMessage extends React.Component<HelloWorldProps, {}> {
render() {
return <div>Hello {this.props.name}</div>;
}
}
React.render(<HelloMessage name="John" />, mountNode);
Wrap-up
While it seemed a bit challenging getting started with TypeScript and JSX I could really see the benefit of the compiler helping out with React components going forward, and look forward to the future of this part of the project.
Thanks to the TypeScript team and community that helped bring all of this support together!
Happy TSXing
!