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}`);
});