todoapp.ts

import React, { Component, MouseEvent, ChangeEvent } from 'react';

type Item = {
  name: string;
  completed: boolean;
};

interface TodoState {
  items: Item[];    // existing items
  newName: string;  // text in the field to add a new name
}

export class ToDoApp extends Component<{}, TodoState> {

  constructor(props: {}) {
    super(props);
    this.state = { items: [], newName: "" };
  }

  componentDidMount() {
    this.refresh();  // get the real data from the server
  }

  // Called to fresh our list of data from the server.
  refresh = () => {
    fetch("/api/list")
        .then(this.handleRefresh)
        .catch(this.handleServerError);
  };

  // Called with the response from a request to /list
  handleRefresh = (res: Response) => {
    if (res.status === 200) {
      res.json().then(this.handleRefreshText)
         .catch(this.handleServerError);
    } else {
      this.handleServerError(res);
    }
  };

  // Called with the text of the response from /list
  handleRefreshText = (vals: unknown) => {
    if (!Array.isArray(vals)) {
      console.error("bad data from /list", vals)
      return;
    }

    const items: Item[] = [];
    for (const val of vals) {
      if ('name' in val && typeof val.name === 'string' &&
          'completed' in val && typeof val.completed === 'boolean') {
        items.push({name: val.name, completed: val.completed});
      } else {
        console.error('bad item in list', val)
      }
    }
    this.setState({items: items});
  };

  // Called when the user checks the box next to an uncompleted item. The
  // second parameter is the index of that item in the list.
  completeItem = (_: ChangeEvent<HTMLInputElement>, index: number): void => {
    const items = this.state.items.slice(0);
    items[index] = {name: items[index].name, completed: true};
    this.setState({items: items});

    fetch("/api/complete?name=" + encodeURIComponent(items[index].name),
          {method: "POST"})
        .then(this.handleCompleted)
        .catch(this.handleServerError)
  }

  // Called when the server confirms that the item was completed.
  handleCompleted = (res: Response) => {
    if (res.status === 200) {
      setTimeout(this.refresh, 5500);   // should be gone then
    } else {
      this.handleServerError(res);
    }
  };

  // Called when the user clicks on the button to add the new item.
  addItem = (_: MouseEvent<HTMLButtonElement>): void => {
    const name = this.state.newName.trim().replace('\t', ' ');  // no tabs
    if (name.length > 0) {
      const items = this.state.items.slice(0);
      items.push({name: name, completed: false});
      this.setState({items: items, newName: ""});

      fetch("/api/add?name=" + encodeURIComponent(name),
            {method: "POST"})
          .then(this.handleAdded)
          .catch(this.handleServerError)
    }
  }

  // Called when the server confirms that the item was added.
  handleAdded = (res: Response) => {
    if (res.status !== 200) {
      this.handleServerError(res);
    }
  };

  // Called each time the text in the new item name field is changed.
  setNewName = (evt: ChangeEvent<HTMLInputElement>): void => {
    this.setState({newName: evt.target.value});
  }

  render = (): JSX.Element => {
    let items : JSX.Element[] = [];
    for (let i = 0; i < this.state.items.length; i++) {
      if (this.state.items[i].completed) {
        items.push(
          <div className="form-check" key={i}>
            <input className="form-check-input" type="checkbox"
                id={"check" + i} checked={true} readOnly={true} />
            <label className="form-check-label completed" htmlFor={"check" + i}>
              {this.state.items[i].name}
            </label>
          </div>);
      } else {
        items.push(
          <div className="form-check" key={i}>
            <input className="form-check-input" type="checkbox"
                id={"check" + i} checked={false}
                onChange={evt => this.completeItem(evt, i)} />
            <label className="form-check-label" htmlFor={"check" + i}>
              {this.state.items[i].name}
            </label>
          </div>);
      }
    }

    return (
      <div>
        <h2>To-Do List</h2>
        {items}
        <p className="instructions">Check the item to mark it completed.</p>
        <p className="more-instructions">New item:
          <input type="text" className="new-item"
              value={this.state.newName}
              onChange={this.setNewName} />
          <button type="button" className="btn btn-link"
              onClick={this.addItem}>Add</button>
        </p>
      </div>);
  }

  // Called when we fail to communicate correctly with the server.
  handleServerError = (_: Response) => {
    // TODO: show the error to the user, with more information
    console.error(`unknown error talking to server`);
  };
}

Full Code