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 alleslintandbabeldependencies - Update
react-scriptsto 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:

Renamed component files: *.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)

Successful start of the development server
Bonus: Missing HMR? Check out this git issue
Bonus #2: Want Redux and linting as well? Check out this next commit