TypeScript の基礎2023年11月5日 | |
TypeScript とはTypeScript は、JavaScript における変数の型に起因した問題を避ける試みのひとつである。JavaScript に型の注釈を導入している。それ以外は概ね JavaScript と同じだが、いくつか拡張されている部分もある。ここでは JavaScript の知識を前提する。 ちなみに、型チェックだけがほしいなら Flow という選択肢もある。 動作環境TypeScript の主な動作環境としては、Node.js や Deno などがある。ここでは Deno を想定する。 インストールLinux (Ubuntu 20.04 LTS) の場合、次のように実行する。 $ curl -fsSL https://deno.land/x/install/install.sh | sh 少し待つと、~/.deno 以下にインストールされる。あとはパスを通せばよい。 ~/.bashrc export DENO_INSTALL="$HOME/.deno" export PATH="$DENO_INSTALL/bin:$PATH" アップグレードは次のようにすればよい。 $ deno upgrade Deno の実行deno と打てば起動する。 Deno 1.37.1 exit using ctrl+d, ctrl+c, or close() REPL is running with all permissions allowed. To specify permissions, run `deno repl` with allow flags. > let a = 1 > a + 1 2 終了するには、Ctrl+D を入力するか、Ctrl+C を 2 回入力するか、close() を入力する。 > close() コードをファイルに記述しておくこともできる (これをスクリプトという)。拡張子は .ts が使われる。 hello.ts
console.log("Hello");
次のように実行する。 $ deno run hello.ts Hello TypeScript をわざわざ使う理由は、型チェックのためである。Deno に型チェックをさせるには、--check を指定する必要がある。 $ deno run --check hello.ts 型チェックだけしたければ、deno check を使う。 $ deno check hello.ts 式JavaScript と同様、TypeScript のコードも式や構文からなり、式はセミコロン (;) で区切られる。多くの場合セミコロンを省略できるところも同じである。 > a = 1 + 2 3 コメント/* ... */ や // ... でコメントを入れることができる。
/* 文字列を表示 */
console.log("Hello");
変数の定義TypeScript の変数は、let で定義する。 > let a = 1 let なしではエラーになる。 変数名は、大文字と小文字が区別される。 var も使える。「巻き上げ」もあるが、型チェックに引っかかる。 console.log(a); var a = 1; 実行
$ deno check var.ts
...
error: TS2454 [ERROR]: Variable 'a' is used before being assigned.
console.log(a);
^
変数への再代入を防ぐ const もある。 > const a = 1 > a = 2 Uncaught TypeError: Assignment to constant variable. 型アノテーションTypeScript の最大の特徴は、変数に型指定が行えることである。これを型アノテーション (注釈) という。 let a: number = 1; これに文字列を入れてみると、型チェックで怒られる。 let a: number = 1; a = "a"; 実行 $ deno check var2.ts ... error: TS2322 [ERROR]: Type 'string' is not assignable to type 'number'. a = "a"; ^ TypeScript は型推論を行うため、上の例では型アノテーションはなくてもよい。 let a = 1; a = "a"; 1 を代入しているため、a は数値型であると正しく推論される。 データ型データの型は typeof で調べられる (関数ではなく演算子)。 > typeof 1 'number' any と unknownany は「何でも型」である。 let a: any = 1; a = "a"; 上のコードは、型チェックに引っかからない。型チェックの意味がなくなるので、any の利用は避ける。 unknown は型の未確定を意味する。型が特定されたときに値を利用できる。 let a: unknown = 1; let b: number = a + 1; 上の例では、2 行目ではまだ a の型が確定していないと判断され、エラーになる。 error: TS18046 [ERROR]: 'a' is of type 'unknown'. let b: number = a + 1; if 文で型を調べたあとであれば、型が特定されたとみなされ、値を使えるようになる。
let a: unknown = 1;
if (typeof a === "number") {
let b: number = a + 1;
}
リテラル型const は値自体が型とみなされる。 const b = true; この場合、b の型は boolean ではなく true と推論される。ただし、typeof で調べるとふつうに boolean と返す。明示的に値を型として指定することもできる。 const b: true = true; これは、const を外してみれば意味がわかる。 let a: 1 = 1; a = 2; これは、次のようなエラーを出す。 error: TS2322 [ERROR]: Type '2' is not assignable to type '1'. a = 2; ^ 数値数値型 (number) は倍精度浮動小数点数である。 let a: number = 1.2; 文字列"..." あるいは '...' で文字列 (string) になる。 let a: string = "Hello"; 論理値false と true を取る論理型 (boolean) がある。 let b: boolean = true; 列挙型JavaScript に列挙型はないが、TypeScript では用意されている。
> enum CatType {
Sabatora,
Chatora,
Mike
}
> let a = CatType.Sabatora
> a
0
> CatType[a]
"Sabatora"
値の型としては数値型である。 数値を指定することもできる。
> enum CatType {
Sabatora = 0,
Chatora = 1,
Mike = 2
}
列挙型は、型アノテーションに使える。 let a: CatType = CatType.sabatora; 値としては数値なのだが、数値を直接入れようとすると型チェックでエラーになる。 シンボル一意の値を作るためにシンボル (symbol) というものがある。
const SABATORA: symbol = Symbol("sabatora");
let a: symbol = SABATORA;
タプルTypeScript にはタプルがある。 let a: [number, number] = [1, 2]; タプルは固定長でそれぞれの型が指定された配列の一種である。上の例では、2 要素のタプルでないと型チェックでエラーになる。 ? をつけると、型をオプションにすることもできる。 let a: [number, number?] = [1, 2]; a = [1]; 上の例では、2 つ目の要素はオプションになっているので、1 要素のタプルでも代入できる。 配列配列の型指定は次のようにする。 let a: number[] = [1, 2, 3]; let b: Array<string> = ["a", "b"]; number[] でも Array<string> でもどちらの方法で型を指定してもよい (違いはない)。 JavaScript の配列では要素の型の混在が許されていたが、TypeScript の場合は、型が混在していることを指定する必要がある。 let c: (number | string)[] = [1, "2"]; 多くの場合は型推論まかせでよいかもしれない。 集合集合 (Set) の型指定は次のようにする。 let a: Set<number> = new Set([1, 2, 2, 0, 5]); 辞書辞書 (Map) の型指定は次のようにする。 let a: Map<string, number> = new Map([["A", 1], ["B", 2]]); オブジェクトJavaScript のオブジェクトの型についての考え方は、ダックタイピングと呼ばれる。これは、構造的型付けともいう。オブジェクトがどういう構造をもつかというのが型になる。
let prof: {name: string, type: string, age: number}
{name: "omochi", type: "sabatora", age: 3};
オブジェクトの「形状」を型として指定している。同じ形状のオブジェクトなら、クラスから作られたものでも代入できる。
class Prof {
name = "";
type = "";
age = 0;
constructor(name: string, type: string, age: number) {
this.name = name;
this.type = type;
this.age = age;
}
}
prof1 = new Prof("annko", "chatora", 4);
上記のクラスを継承したものでも代入できる。
class Prof2 extends Prof {
food: string[] = [];
constructor(name: string, type: string, age: number) {
super(name, type, age);
this.food = [];
}
}
prof1 = new Prof2("annko", "chatora", 4);
その逆はできない。
let prof2: Prof2 = new Prof("annko", "chatora", 4);
error: TS2741 [ERROR]: Property 'food' is missing in type 'Prof' but required in type 'Prof2'.
let prof2: Prof2 = new Prof("annko", "chatora", 4);
~~~~~
ただ、この場合は food というフィールドの問題なので、「food はあってもなくてもいいよ」という指定をすればいける。
class Prof2 extends Prof {
food?: string[] = [];
constructor(name: string, type: string, age: number) {
super(name, type, age);
this.food = [];
}
}
let prof2: Prof2 = new Prof("annko", "chatora", 4);
food? で food がなくても受け入れられるようになる。 空のオブジェクトで型推論をさせると、何も受け付けなくなるわけではなく、逆になんでもありになってしまう。
let prof1 = {};
prof1 = new Prof("annko", "chatora", 4);
prof1 = new Prof2("annko", "chatora", 4);
型チェックの意味がなくなるため、空のオブジェクトを型指定なしに用いるのは避けたほうがよい。 インデックスシグネチャフィールドの型が均質で、任意の数になりうるのであれば、インデックスシグネチャというもので前もって予告しておくことができる。
class C {
[key: string]: string:
...
}
key というのは任意の名前で、キーの型は string か number である。上の例では、クラス C のオブジェクトについては、string 型のすべてのキーは string 型の値を持たなければならない、という意味になる。 型エイリアスtype で新しい型を作ることができる。
type Age = number;
type CatType = string;
type Prof = {
name: string,
type: CatType,
age: Age
};
let prof: Prof = {
name: "omochi",
type: "sabatora",
age: 3
};
型の合併 (union) を作ることもできる。 type NumOrStr = number | string; let a: NumOrStr[] = [1, "2"]; 交差 (&) もある。型をまぜこぜするらしい。
type Prof = {
name: string,
age: number
};
type CatType = {
type: string
};
type CatProf = Prof & CatType;
let prof: CatProf = {
name: "omochi",
type: "sabatora",
age: 3
};
型エイリアスの一意性型エイリアスは型の名前を付け直しているだけなの、問題が起こることがある。
type CatType = string;
type Food = string;
function catTypeCheck(t: CatType) {};
let f: Food = "karikari";
catTypeCheck(f);
上記の最後の関数の呼び出しは、型の名前は異なるにもかかわらず、同じ string なのでエラーにならない。 型チェックが効くようにするには、次のようにする。
type CatType = string & {readonly brand: unique symbol};
type Food = string & {readonly brand: unique symbol};
ただし、これだと Food 型の変数に文字列が入らなくなるので、次のようにする。 let f: Food = "karikari" as Food; これを型アサーションという。上の場合は、string を Food と解釈しなさい、という指示になる。 undefined、null、void、neverundefined、null の型は、それぞれそのまま undefined、null である (typeof の値自体は、null のほうは "object")。 TypeScript にはこれらの他に void、never と呼ばれているものがある。void は return なく終了する関数の型を意味する (実際は undefined を返すし、typeof の値は当然 "undefined" だけども)。never は、例外でも発生しない限り終了しない関数の型を意味する。 never はまた、すべての型のサブタイプでもある。 プログラムの停止プDeno では、Deno.exit() でプログラムが停止する。 Deno.exit(); 引数として終了コードをとる。 関数関数における型指定は次のように行う。
function f(x: number, y: number): number {
return x + y;
}
function g() {
console.log(f(1, 2));
}
オプション引数引数に ? を付けることで、その引数をオプションにできる。
function f(x: number, y?: number) {
...
}
ただ、ふつうはデフォルト値指定のほうが使いやすいかもしれない。 型エイリアス関数の型エイリアスは次のようにする。 type NumOp2 = (x: number, y: number) => number; let f: NumOp2 = (x, y) => x + y; 2 行目の左辺では、引数の型指定をしていない。関数の型から推定できるためである。これを文脈的型付けという。これは、コールバック関数を定義するのに便利である。
function callFunc(callback: NumOp2, x: number, y: number) {
console.log(callback(x, y));
}
callFunc((x, y) => x + y, 1, 2);
文脈的型付けのおかげで、コールバック関数の型アノテーションを省くことができる。 オーバーロードTypeScript は、静的型付けを導入したおかげで、関数のオーバーロードが一応、可能になっている。一応、というのは、ここでのオーバーロードは多重定義というより、複数のシグネチャ (関数名と引数型のセット) に対応できる、という意味だからである。実装自体はひとつしかないので、ごちゃまぜの型で頑張ってやるしかない。
function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x: number | string, y: number | string): number | string {
if (typeof x === "number" && typeof y === "number") {
return x + y;
} else if (typeof x === "string" && typeof y === "string") {
return x + y;
} else {
throw new Error("Argument type error");
}
}
console.log(add(1, 2));
console.log(add("a", "b"));
1、2 行目がシグネチャのリストで、3 行目以降が実装である。実装では、全シグネチャを受け取れるような関数を用意することになる。当然、シグネチャのリストなしでも関数はふつうに呼べるが、シグネチャを置いておくと、それに合わない呼び出しは型チェックでエラーにしてくれる。 console.log(add(1, "a")); エラー
error: TS2769 [ERROR]: No overload matches this call.
Overload 1 of 2, '(x: number, y: number): number', gave the following error.
Argument of type 'string' is not assignable to parameter of type 'number'. Overload 2 of 2, '(x: string, y: string): string', gave the following error.
Argument of type 'number' is not assignable to parameter of type 'string'.
console.log(add(1, "a"));
~~~~~~~~~~~
ところで、number も string も + 演算子を受け付けるので、場合分けをしなくてもよいのでは、と思うかもしれないが、unknwon と事情は同じで、number | string の状態では型が決まっていないので、「+ 演算子は使えないよ」と文句を言われるため、typeof で調べて型を確定してやる必要がある。 型エイリアスでオーバーロードに対応するには、次のようにする。
type Add = {
(x: number, y: number): number | string;
(x: string, y: string): number | string;
};
let add: Add = (
x: number | string,
y: number | string
) => {
if (typeof x === "number" && typeof y === "number") {
return x + y;
} else if (typeof x === "string" && typeof y === "string") {
return x + y;
} else {
throw new Error("Argument type error");
}
}
function の方法と異なり、返り値の型は統一しなければならないようである。 ジェネリック型をパラメータに取るジェネリックというものがある (C++ のテンプレートのようなもの)。ジェネリックで上の add() を実装すると、次のようになる。
function add<T extends number | string>(x: T, y: T): unknown {
if (typeof x === "number" && typeof y === "number") {
return x + y;
} else if (typeof x === "string" && typeof y === "string") {
return x + y;
} else {
throw Error("Argument type error");
}
}
console.log(add(1, 2));
console.log(add("a", "b"));
T は任意の名前で、型のパラメータである。<T> で型のパラメータを設定するのだが、ここでは extends で型の種類を制限している。戻り値の型は T でもよさそうに思えるが、T は number あるいは string のサブタイプでもよいため、「x + y の型は number あるいは string とは限らない」などと怒られる。しかたないので、unknown としている。 型ガード曖昧な型に対して、typeof などで分岐させると、TypeScript は型を確定したものとして扱ってくれる。これは型ガードと呼ばれる。この機能を自分で実装したい場合はどうか。たとえば、型を確認する関数を定義したとしても、機能してくれない。
function isNumber(x: unknown): boolean { return typeof x === "number"; }
function isString(x: unknown): boolean { return typeof x === "string"; }
function add<T extends number | string>(x: T, y: T): unknown {
if (isNumber(x) && isNumber(y)) {
return x + y;
} else if (isString(x) && isString(y)) {
return x + y;
} else {
throw Error("Argument type error");
}
}
これは、「+ が使えない」としてエラーになる。この場合、次のようにする。
function isNumber(x: unknown): x is number { return typeof x === "number"; }
function isString(x: unknown): x is string { return typeof x === "string"; }
x is number などで、関数が型ガードであることを明示している。 クラスアクセス修飾子次のようなクラスを考える。
class Prof {
constructor(name, type, age) {
this.set_name(name);
this.set_type(type);
this.set_age(age);
}
set_name(name) { this.name = name; }
set_type(type) { this.type = type; }
set_age(age) { this.age = age; }
show_profile() {
console.log(`name: ${this.name}`);
console.log(`type: ${this.type}`);
console.log(`age: ${this.age}`);
}
}
これは、次のように書ける。
class Prof {
constructor(
private name: string,
private type: string,
private age: number
) {}
...
ここで、private をアクセス修飾子という。ここでは name、type、age をそれぞれ自動的に this に割り当て、アクセスをプライベートなものにする。つまり、クラスの外部では this.name などでアクセスできなくなる。 アクセス修飾子は、private の他に、protected、public がある。protected は、そのクラスのサブクラスからアクセスできるようにする。public はクラスの外からでもアクセスできる。 コンストラクタで指定しない変数については、変数定義のところでアクセス修飾子を付けられる。
class Prof {
private food = [];
...
なにも付けなければ public になる。 抽象クラス他のクラスのベースにしたいが、インスタンス化はさせたくない場合は、抽象クラスというものが使える。class の前に abstract を付ければよい。
abstract class Prof0 {
...
これで、継承はできるが、new でインスタンス化しようとすると怒られるようになる。 自分自身を返すメソッドで自分自身を返したい場合の型はどうするか。クラス名でもよいが、それだと継承したときにいちいちオーバーライドしなくてはならなくなる。そいうい場合は、this を指定すればよい。
class C {
...
f(x: number): this {
...
インターフェイスオブジェクトの形状を型にするのに、型エイリアスがあった。
type Prof = {
name: string,
age: number
};
type CatProf = Prof & {
type: string
};
let prof: CatProf = {
name: "omochi",
type: "sabatora",
age: 3
};
これと似たもので、インターフェイスというものがある。
interface Prof {
name: string,
age: number
}
interface CatProf extends Prof {
type: string
}
let prof: CatProf = {
name: "omochi",
type: "sabatora",
age: 3
};
インターフェイスを実装したクラスを作ることができる。
class Cat implements CatProf {
constructor(
public name: string,
public type: string,
public age: number
) {}
}
クラスの実装のベースとなるという意味で、インターフェイスと抽象クラスは似ているが、抽象クラスはデフォルトの実装やアクセス制御を提供できるのに対し、インターフェイスはそれができないという違いがある。 モジュールDeno では ECMAScript modules の形式のモジュールが使える。 標準モジュールなどを利用する場合は、次のようにする。
import { CsvStringifyStream } from "https://deno.land/std@0.205.0/csv/mod.ts";
まず、Deno ではモジュールを URL で指定できる。"std@0.205.0" の形でバージョン指定でき、これによりバージョンを固定できる。バージョン指定は任意だが、推奨されている。パッケージの明示的なインストールは必要ではなく、実行時に自動的にダウンロードされる。この仕組みにより、Node.js でのパッケージの仕組みが簡易化されてている。 Node.js のモジュールも使える。
import { writeFileSync, readFileSync } from "node:fs";
自分で作成したモジュールを使う場合は、以下のようにする。たとえば、以下のようなモジュールを用意したとする。 fibonacci.ts
export default { fibonacci };
function fibonacci(n: number): number {
if (n === 0) return 0
else if (n === 1) return 1
else return fibonacci(n - 2) + fibonacci(n - 1)
}
これを用いるには、次のようにする。 import fibonacci from "./fibonacci.ts"; console.log(fibonacci.fibonacci(10)); コマンドライン引数Deno をシェルで実行するときにスクリプトと引数を指定した場合、それらは Deno.args で取得できる。 args.js console.log(Deno.args); 実行 $ deno run --check args.ts 1 2 3 Check file:///home/yuyu/work/typescript/args.ts [ "1", "2", "3" ] ファイルDeno でファイルの読み書きをするには、たとえば次のようにする。
const lines = ["aaa\n", "bbb\n", "ccc"];
Deno.writeFileSync("test.txt", new TextEncoder().encode(lines.join("")));
const lines2 = Deno.readFileSync("test.txt");
console.log(new TextDecoder().decode(lines2));
writeFileSync()、readFileSync() は Uint8Array を扱うため、TextEncoder()、TextDecoder() で変換している。 実行の際には、セキュリティのために書き込み・読み込みに制限がされており、オプションで許可を与える必要がある。 $ deno run --check --allow-write --allow-read file.ts aaa bbb ccc テキストファイルを直接扱う writeTextFileSync()、readTextFileSync() というのもある。
const lines = ["aaa\n", "bbb\n", "ccc"];
Deno.writeTextFileSync("test.txt", lines.join(""));
const lines2 = Deno.readTextFileSync("test.txt");
console.log(lines2);
1行ずつ処理したい場合は、次のような方法がある。
const lines = ["aaa\n", "bbb\n", "ccc"];
const file = await Deno.open("test.txt", { write: true });
const encoder = new TextEncoder();
const writer = file.writable.getWriter();
for await (const line of lines) {
writer.write(encoder.encode(line));
}
const file2 = await Deno.open("test.txt", { read: true });
const decoder = new TextDecoder();
for await (const line of file2.readable) {
console.log(decoder.decode(line));
}
CSV の読み書きには、標準モジュールが使える。 書き出し
import { CsvStringifyStream } from "https://deno.land/std@0.205.0/csv/mod.ts";
const file = await Deno.open("data.csv", { create: true, write: true });
const readable = ReadableStream.from([
{ id: 1, name: "omochi" },
{ id: 2, name: "annko" },
{ id: 3, name: "tamago" },
]);
await readable
.pipeThrough(new CsvStringifyStream({ columns: ["id", "name"] }))
.pipeThrough(new TextEncoderStream())
.pipeTo(file.writable);
読み込み
import { CsvParseStream } from "https://deno.land/std@0.205.0/csv/mod.ts";
const file = await Deno.open("data.csv", { read: true });
const readable = ReadableStream.from(file.readable);
for await (const line of readable
.pipeThrough(new TextDecoderStream())
.pipeThrough(new CsvParseStream())) {
console.log(line);
}
参考
| |
| PENGUINITIS | |