React のコードを JavaScript から TypeScript に変える

型が無い言語は人類には早すぎると思っているので、JavaScript で書いてある React のコードを TypeScript にした。

とはいえ TypeScript を書いたことが一度も無いので、コンパイル時にエラーが出るところを一つずつ調べて潰す方法で書き換えた。

元になったJS のコードがこれ。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
import React, { useState, useEffect } from 'react';
import './App.css';
import firebase from 'firebase/app';
import 'firebase/firestore';
import dayjs from 'dayjs';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_DATABASE_URL,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID
};

firebase.initializeApp(firebaseConfig);

const db = firebase.firestore();

function Blog(props) {
  const items = props.items;
  return (
    <ol>
      {items.map((item) => {
        const created = dayjs(item.created.toDate());
        const updated = dayjs(item.updated.toDate());
        return (
          <li key={item.id}>
            <div>{item.id}</div>
            <div className="App-text">{item.text}</div>
            <div>
              <time dateTime={created.format('YYYY-MM-DDTHH:mm:ss')}>
                {created.format('YYYY-MM-DD HH:mm:ss')}
              </time>
            </div>
            <div>
              <time dateTime={updated.format('YYYY-MM-DDTHH:mm:ss')}>
                {updated.format('YYYY-MM-DD HH:mm:ss')}
              </time>
            </div>
          </li>
        );
      })}
    </ol>
  );
}

function NextButton(props) {
  const hasNext = props.hasNext;
  if (hasNext) {
    return <button onClick={() => props.onClick()}>次へ</button>;
  }
  return null;
}

function Main() {
  const [items, setItems] = useState([]);
  const [lastItem, setLastItem] = useState(null);
  const [hasNext, setHasNext] = useState(false);
  const [loadNext, setLoadNext] = useState(true);

  useEffect(() => {
    async function getItems() {
      let first;
      if (hasNext) {
        first = await db.collection("posts").orderBy('created', 'desc').startAfter(lastItem.data().created).limit(5);
      } else {
        first = await db.collection("posts").orderBy('created', 'desc').limit(5);
      }
      const snapshot = await first.get();
      const last = snapshot.docs[snapshot.docs.length - 1];
      const next = await db.collection("posts").orderBy('created', 'desc').startAfter(last.data().created).limit(1);
      const nextSnapshot = await next.get();
      snapshot.forEach((doc) => {
        let item = doc.data();
        item.id = doc.id;
        items.push(item);
      });
      setItems(items);
      setLastItem(last);
      setHasNext(nextSnapshot.docs.length > 0 ? true : false);
    }
    if (loadNext) {
      getItems();
      setLoadNext(false);
    }
  }, [items, lastItem, hasNext, loadNext]);

  return (
    <main>
      <Blog items={items} />
      <NextButton hasNext={hasNext} onClick={() => setLoadNext(true)}/>
    </main>
  );
}

function App() {
  return (
    <div>
      <Main />
    </div>
  );
}

export default App;

とにかくエラーが出ないようにというところだけ意識して TS に書き換えたのがこれ。
差分ハイライトしている。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import React, { useState, useEffect } from 'react';
import './App.css';
import firebase from 'firebase/app';
import 'firebase/firestore';
import dayjs from 'dayjs';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  databaseURL: process.env.REACT_APP_DATABASE_URL,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_APP_ID
};

firebase.initializeApp(firebaseConfig);

const db = firebase.firestore();

type BlogItem = {
  id: string,
  text: string,
  created: firebase.firestore.Timestamp,
  updated: firebase.firestore.Timestamp
}

type BlogProps = {
  items: BlogItem[]
}

type NextButtonProps = {
  hasNext: boolean,
  onClick: React.MouseEventHandler<HTMLButtonElement>
}

function Blog(props: BlogProps) {
  const items = props.items;
  return (
    <ol>
      {items.map((item) => {
        const created = dayjs(item.created.toDate());
        const updated = dayjs(item.updated.toDate());
        return (
          <li key={item.id}>
            <div>{item.id}</div>
            <div className="App-text">{item.text}</div>
            <div>
              <time dateTime={created.format('YYYY-MM-DDTHH:mm:ss')}>
                {created.format('YYYY-MM-DD HH:mm:ss')}
              </time>
            </div>
            <div>
              <time dateTime={updated.format('YYYY-MM-DDTHH:mm:ss')}>
                {updated.format('YYYY-MM-DD HH:mm:ss')}
              </time>
            </div>
          </li>
        );
      })}
    </ol>
  );
}

function NextButton(props: NextButtonProps) {
  const hasNext = props.hasNext;
  if (hasNext) {
    return <button onClick={props.onClick}>次へ</button>;
  }
  return null;
}

function Main() {
  const [items, setItems] = useState<BlogItem[]>([]);
  const [lastItem, setLastItem] = useState<firebase.firestore.QueryDocumentSnapshot>();
  const [hasNext, setHasNext] = useState(false);
  const [loadNext, setLoadNext] = useState(true);

  useEffect(() => {
    async function getItems() {
      let first;
      if (hasNext && lastItem && lastItem.data()) {
        first = await db.collection("posts").orderBy('created', 'desc').startAfter(lastItem.data().created).limit(5);
      } else {
        first = await db.collection("posts").orderBy('created', 'desc').limit(5);
      }
      const snapshot = await first.get();
      const last = snapshot.docs[snapshot.docs.length - 1];
      const next = await db.collection("posts").orderBy('created', 'desc').startAfter(last.data().created).limit(1);
      const nextSnapshot = await next.get();
      snapshot.forEach((doc) => {
        let item = doc.data();
        const blogItem: BlogItem = {
          id: doc.id,
          text: item.text,
          created: item.created,
          updated: item.updated
        }
        items.push(blogItem);
      });
      setItems(items);
      setLastItem(last);
      setHasNext(nextSnapshot.docs.length > 0 ? true : false);
    }
    if (loadNext) {
      getItems();
      setLoadNext(false);
    }
  }, [items, lastItem, hasNext, loadNext]);

  return (
    <main>
      <Blog items={items} />
      <NextButton hasNext={hasNext} onClick={() => setLoadNext(true)}/>
    </main>
  );
}

function App() {
  return (
    <div>
      <Main />
    </div>
  );
}

export default App;

まだエラーが出ないようにしただけなので、変な部分は残ってると思う。

詰まった点は

  • 本質ではないんだけど、新規で create-react-app してから firestore をインストールしたら v9.0.0 が入ってしまい、v9.0.0 から色々変わって v8.9.0 以前と同じ書き方だと型エラーが出るようになっていたのでバージョンを下げた
  • onClick の型がわからない
    • 結局 VSCode の型推論を見て書いた
  • Firestore から取ってきたデータを自作の型で宣言した変数に詰め直さなければならないのかがわからない

あたりで、特に最後の点は TypeScript を勉強しないといけないと思う。