FullStackOpen Part9-b First steps with TypeScript メモ

Setting things up

VSCodeではネイティブでTypeScriptをサポートしている
Nodeでtypescriptとts-nodeをインポートして追加しておく

npm install -g ts-node typescript

package.jsonにも追加しておく

{
  // ..
  "scripts": {
    "ts-node": "ts-node"
  },
  // ..
}

コマンドからファイルの実行をする場合は以下のように。

npm run ts-node file.ts -- -s --someoption

A note about coding style

Javascriptは緩いルールで書くことができたが、TypeScriptはより厳格なルールで書くことができる
ルールは./tsconfig.jsonで設定する
ひとまずnoImplicitAnyというオプションを無効化だけしておく

{
  "compilerOptions":{
    "noImplicitAny": false
  }
}

multiplier.tsを作成(TypeScriptは拡張子ts)

const multiplicator = (a, b, printText) => {
  console.log(printText,  a * b);
}

multiplicator(2, 4, 'Multiplied numbers 2 and 4, the result is:');

これをTypescriptで書くとこうなる(primitiveであるnumber string booleanのうちnumberとstringを使用)

const multiplicator = (a: number, b: number, printText: string) => {
  console.log(printText,  a * b);
}

multiplicator('how about a string?', 4, 'Multiplied a string and 4, the result is:');

Creating your first own types

TypeScriptではプリミティブな型に加えて自分でデータ型を作ることができる
typeというキーワードを使って作る

type Operation = 'multiply' | 'add' | 'divide';

Operation"型"は三種類の文字列のみを受け付ける。
OR演算子"|"を使ってユニオン型を作ることができる

typeキーワードを使うことで型エイリアスを作ることができる
以下のようにプリミティブ型を複数受け付けるような型を作成することも可能。
type stringAndNumber = 'number' | 'string';

四則演算をする関数をTypeScriptで書くとこんな感じ
ゼロ除算を防ぐためにthrow new Errorを使用

type Operation = 'multiply' | 'add' | 'divide';


const calculator = (a: number, b: number, op: Operation) : number => {
  switch(op) {
    case 'multiply':
      return a * b;
    case 'divide':

      if (b === 0) throw new Error('Can\'t divide by 0!');
      return a / b;
    case 'add':
      return a + b;
    default:

      throw new Error('Operation is not multiply, add or divide!');
  }
}

try {
  console.log(calculator(1, 5 , 'divide'));
} catch (error: unknown) {
  let errorMessage = 'Something went wrong: '
  if (error instanceof Error) {
    errorMessage += error.message;
  }
  console.log(errorMessage);
}

Type narrowing

上記の例のerrorのmessageプロパティにアクセスするときに、instanceofを使用している。
もともとunknown型として定義しているerrorに対し、Errorクラスから作成されたインスタンスであることを確認してから、error.messageにアクセスすることで型セーフを実現している

instanceof以外だとasといったものを使える

例:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

error.messageにアクセスできるのは以下の場所となる

try {
  console.log(calculator(1, 5 , 'divide'));
} catch (error: unknown) {
  let errorMessage = 'Something went wrong: '
  // here we can not use error.message

  if (error instanceof Error) {
    // the type is narrowed and we can refer to error.message

    errorMessage += error.message;
  }
  // here we can not use error.message

  console.log(errorMessage);
}

Accessing command line arguments

古いnodeのバージョンだとprocess.argvを使うとエラーがでる
これは以下の@types/nodeが足りていないためである

@types/{npm_package}

TypeScriptで記述されたパッケージは通常@types/から始まる
例えば:

npm install --save-dev @types/react @types/express @types/lodash @types/jest @types/mongoose

Typeはコンパイル前にのみ有効なので、常に--save-devでインストールする

Improving the project

Multiplierを改善するとこうなる

interface MultiplyValues {
    value1: number;
    value2: number;
}

const parseArguments = (args: string[]): MultiplyValues => {
    if (args.length < 4) throw new Error("Not enough arguments");
    if (args.length > 4) throw new Error("Too many arguments");

    if (!isNaN(Number(args[2])) && !isNaN(Number(args[3]))) {
        return {
            value1: Number(args[2]),
            value2: Number(args[3])
        }
    } else {
        throw new Error("Provided values were not numbers!");
    }
}

const multiplicator = (a: number, b: number, printText: string) => {
    console.log(printText, a * b);
}

try {
    const { value1, value2 } = parseArguments(process.argv);
    multiplicator(value1, value2, `Multiplied ${value1} and ${value2}, the result is `);
} catch (error: unknown) {
    let errorMessage = 'Something went wrong: ';
    if (error instanceof Error) {
        errorMessage += error.message;
    }
    console.log(errorMessage)
}

まずinterfaceキーワードを使って、オブジェクトの構造を指定して定義する

コマンドラインからargvを受け取り、長さをチェックする。
このようにすることでイレギュラーな値が入ってきてもはじけるようになる。

The alternative array syntax

typescriptでは二通りの配列定義の方法がある

let values: Array<number>;

let values: number[];

ジェネリックのArray<number>を使う。

More about tsconfig

tsconfig.jsonの設定を変更。
詳細は後で出てくるらしい。

{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    "noImplicitAny": true,
    "esModuleInterop": true,
    "moduleResolution": "node"
  }
}

Adding Express to the mix

ExpressサーバーをTypeScriptで書くとこんな感じ

import express from 'express';
const app = express();

app.get('/ping', (_req, res) => {
    res.send('pong');
});

const PORT = 3003;

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

ポイントは:
TypeScriptではモジュールは基本importする
使わない変数はどうしても出てくるので、どうしようもない場合はアンダーバーを前に置くとエスケープできる(_reqみたいに)

また以下のパッケージをインストール
npm install express
npm install --save-dev @types/express (expressのタイプ)
npm install --save-dev ts-node-dev (NodemonのTypescript版)

スクリプトに追加しておく

{
  // ...
  "scripts": {
      // ...

      "dev": "ts-node-dev index.ts",
  },
  // ...
}

The horrors of any

any型にはImplicitなanyとExplicitなanyがある。
どちらも挙動は変わらないが、意図しないanyは嫌われる。
例えば以下の例だと、reqのボディからとってきた値はanyとなる

import { calculator } from './calculator';

app.use(express.json());

// ...

app.post('/calculate', (req, res) => {
  const { value1, value2, op } = req.body;

  const result = calculator(value1, value2, op);
  res.send({ result });
});

そこでImplicitなany型を除外するためにtsconfig.jsonで、noImplicitAnyを有効化しておく。

しかしnoImplicitAnyを有効化しているのにもかかわらず、req.bodyでエラーを吐かない。
これはRequestのbodyは明示的にanyにしているため。

これらをチェックするためにEslintを使う。
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser

packageにlintを追加

{
  // ...
  "scripts": {
      "start": "ts-node index.ts",
      "dev": "ts-node-dev index.ts",

      "lint": "eslint --ext .ts ."
      //  ...
  },
  // ...
}

またtsconfigも編集しておく

{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking"
  ],
  "plugins": ["@typescript-eslint"],
  "env": {
    "node": true,
    "es6": true
  },
  "rules": {
    "@typescript-eslint/semi": ["error"],
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "@typescript-eslint/restrict-template-expressions": "off",
    "@typescript-eslint/restrict-plus-operands": "off",
    "@typescript-eslint/no-unused-vars": [
      "error",
      { "argsIgnorePattern": "^_" }
    ],
    "no-case-declarations": "off"
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "project": "./tsconfig.json"
  }
}

これでreq.bodyもエラーになるようになった。
今の時点ではeslint-disable-next-lineで無視するようにしておく

app.post('/calculate', (req, res) => {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  const { value1, value2, op } = req.body;


  if ( !value1 || isNaN(Number(value1)) ) {
    return res.status(400).send({ error: '...'});
  }

  // more validations here...

  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
  const result = calculator(Number(value1), Number(value2), op);
  return res.send({ result });
});

Type assertion

calculator.tsのtype Operationをエクスポートして他から参照できるようにする。

export type Operation = 'multiply' | 'add' | 'divide';

これを使用して型アサーションができる。
あまり推奨された方法ではないがこのようにすることでESlintを黙らせることができる

app.post('/calculate', (req, res) => {
    //eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const { value1, value2, op } = req.body;

    if (!value1 || isNaN(Number(value1))) {
        return res.status(400).send({
            error: 'Invalid value1!'
        });
    }
    
    //assert the type
    const operation = op as Operation;

    const result = calculator(Number(value1), Number(value2), operation);
    return res.send({ result });
});

演習の気づき

演習はこんな感じ

import express from 'express';
const app = express();

import { calculateBmi } from './bmiCalculator';
import { calculator, Operation } from './calculator';
import { ExerciseDetail, calculateExercise } from './exerciseCalculator';

app.use(express.json());

app.get('/ping', (_req, res) => {
    res.send('pong');
});

app.get('/hello', (_req, res) => {
    res.send('Hello fullstack!');
});

app.get('/bmi', (req, res) => {
    if (req.query.height && req.query.weight) {
        res.send({
            height: req.query.height,
            weight: req.query.weight,
            bmi: calculateBmi(Number(req.query.height), Number(req.query.weight))
        });
    } else {
        res.status(400).send({
            error: 'Height or weight is missing!'
        });
    }
});

app.post('/calculate', (req, res) => {
    //eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const { value1, value2, op } = req.body;

    if (!value1 || isNaN(Number(value1))) {
        return res.status(400).send({
            error: 'Invalid value1!'
        });
    }

    //assert the type
    const operation = op as Operation;

    const result = calculator(Number(value1), Number(value2), operation);
    return res.send({ result });
});

app.post('/exercises', (req, res) => {
    //eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    if (!req.body.daily_exercises || !req.body.target) return res.status(400).send({ error: 'Missing parameters!' });
    //eslint-disable-next-line
    if (!(Array.isArray(req.body.daily_exercises)) || !(req.body.daily_exercises.every((item: any) => typeof item === 'number'))) return res.status(400).send({ error: 'Invalid parameter!' });

    //eslint-disable-next-line
    const daily_exercises: Array<number> = req.body.daily_exercises;

    //eslint-disable-next-line
    const target: number = req.body.target;

    const result: ExerciseDetail = calculateExercise(daily_exercises, target);
    return res.send(result);
});

const PORT = 3003;

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});


この記事が気に入ったらサポートをしてみませんか?