Eli Weinstock-Herman

Converting the ASP.Net Core 2.2 React SPA to Typescript

February 27, 2019 ▪ technical posts

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.

Versions in use:
  • 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 all eslint and babel 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

package.json

{
  "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:

tsconfig.json

{
  "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 files with *.tsx extensions
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

Counter.tsx

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:

FetchData.tsx


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:

index.tsx

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
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

Share: