ASP.Net Core 2.2's react template now starts us in with similar
content to npx create-react-app
. I wanted to use TypeScript for a project,
so here's the steps and resources I used to convert the React app to TypeScript.
- ASP.Net Core 2.2
- React 16.2
- TypeScript 3.3.3
First let's get our dependencies correct. For this I used a post from Jon Hilton:
- In your
package.json
, remove alleslint
andbabel
dependencies - Update
react-scripts
to the latest version npm install -D @types/node @types/react @types/react-dom @types/jest @types/react-router @types/react-router-dom
- (optional) if you plan to keep bootstrap, also
npm install -D @types/reactstrap
{
"name": "BlogExample.Web",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^4.1.3",
"jquery": "3.3.1",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-router-bootstrap": "^0.24.4",
"react-router-dom": "^4.2.2",
"react-scripts": "^2.1.3",
"reactstrap": "^6.3.0",
"rimraf": "^2.6.2"
},
"devDependencies": {
"@types/jest": "^24.0.0",
"@types/node": "^10.12.24",
"@types/react": "^16.8.2",
"@types/react-dom": "^16.8.0",
"@types/react-router": "^4.4.3",
"@types/react-router-dom": "^4.3.1",
"@types/reactstrap": "^7.1.3",
"ajv": "^6.0.0",
"cross-env": "^5.2.0",
"typescript": "^3.3.3"
},
"eslintConfig": {
"extends": "react-app"
},
"scripts": {
"start": "rimraf ./build && react-scripts start",
"build": "react-scripts build",
"test": "cross-env CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
Now we need to get TypeScript compiling correctly. We can borrow from the typescript site to add a tsconfig.json file:
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"noImplicitAny": true
},
"include": [
"src"
],
"compileOnSave": true
}
I've included compilerOptions.noImplicitAny
and compileOnSave
to enforce some strictness
Next we're ready to convert our JSX files, so rename all of the app .js/.jsx files to *.tsx:
At this point, building the application will produce several errors.
In Counter.tsx
, we need to define the shape of the local state for the component. So let's add an interface
interface ILocalState {
currentCount: number
}
export class Counter extends Component<{}, ILocalState> {
static displayName = Counter.name;
constructor (props: any) {
// ...
}
FetchData also needs a defined type for it's state and for the forecasts passed to renderForecastsTable
:
interface ILocalState {
forecasts: IForecast[],
loading: boolean
}
interface IForecast {
dateFormatted: string,
temperatureC: number,
temperatureF: number,
summary: string
}
export class FetchData extends Component<{}, ILocalState> {
static displayName = FetchData.name;
constructor (props: any) {
super(props);
this.state = { forecasts: [], loading: true };
fetch('api/SampleData/WeatherForecasts')
.then(response => response.json())
.then(data => {
this.setState({ forecasts: data, loading: false });
});
}
static renderForecastsTable (forecasts: IForecast[]) {
// ...
}
// ...
}
The final issue is with the baseUrl for the Router. When we get the URL from the page it is implicitly typed as string | undefined
while the basename expects string | null
. We can cast the value to solve this:
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href') as string;
Run it! Everything should now be running.
Wrapping Up
You can see the full set of changes here: github (note: You want all of the BlogExample/*
changes, ignore the vanilla-react-app
folder)
Bonus: Missing HMR? Check out this git issue
Bonus #2: Want Redux and linting as well? Check out this next commit