TypeScript 基本¶
ここでは、公式ドキュメントをいかに短くまとめるかをモットーに記載しています。
型エイリアスとインターフェースの違い¶
- 型エイリアス:
type
- インターフェース:
interface
Note
型は新しいプロパティを追加するために再度オープンできないのに対し、
インターフェースは常に拡張可能であることです。
拡張¶
// インターフェース
interface Animal {
name: string;
}
interface Bear extends Animal {
honey: boolean;
}
// 型エイリアス
type Animal = {
name: string;
};
type Bear = Animal & {
honey: boolean;
};
新しいフィールドを追加する¶
// インターフェース
interface Window {
title: string;
}
interface Window {
ts: TypeScriptAPI;
}
// 型エイリアス
type Window = {
title: string;
};
type Window = {
ts: TypeScriptAPI;
};
// * Error: Duplicate identifier 'Window'.
インターセクション¶
2 つ以上のインターフェースを組み合わせて型を定義することもできる。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
インターセクション
を使って以下のように書くこともできる。
type ColorfulCircle = Colorful & Circle;
同じプロパティでも型が同じであればマージされるが、互換性がないとエラーになる。
関数型¶
関数のtype
は以下のように定義できる。
// `args` を引数に取って、何も返さない(void)関数
type GreetFunction = (args: string) => void;
type GreetFunction = {
(args: string): void;
};
型アサーション¶
as
を使ったこういうやつ。コンパイラによって削除される。
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
ナローイング¶
複数の型を取りうる変数に対して、型を絞ること
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
// ここで padding の型を絞っている。
return " ".repeat(padding) + input;
}
// この段階では padding は string型しか取り得ない。
return padding + input;
}
真実性の狭まり¶
if では以下の値を全てfalse
とみなす。
- 0
- NaN
- ""(空の文字列)
- 0n(bigint ゼロバージョン)
- null
- undefined
型操作¶
型述語¶
型の判定はis
を使って行える
typeof パラメータ名 is 型名 // boolean
ジェネリクス¶
型を変数のように扱える仕組み
→ 引数に応じてアウトプットの型を決める時などに使用する。
function 関数名<型変数>(引数: 型変数): 型変数 {
// 処理
}
例えば以下の例
function firstElement(arr: any[]): any {
return arr[0]; // 型情報が失われる
}
色んな型の変数を受け取りたいから上記のように実装したが、アウトプットの型がany
になってしまう。
かと言って、型に応じて以下のように関数を分けるのも冗長。
function firstElementString(arr: string[]): string | undefined { ... }
function firstElementNumber(arr: number[]): number | undefined { ... }
そこで使用するのがジェネリクス
。
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0]; // 型安全で汎用的
}
// 以下のように書くことも。<>の中の文字列は任意
function identity<T>(value: T): T {
return value;
}
// 自作map関数の例は以下
function map<Input, Output>(
arr: Input[],
func: (arg: Input) => Output
): Output[] {
return arr.map(func);
}
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
< >
内で型の変数を宣言し、関数内でその型変数を使って型安全な汎用関数を作る仕組み。
< >
の中は何でもいい。
Tip
< >
はジェネリクスでしか使わないので、これが出てきたら「ジェネリクス使ってるんだな」と思えばいい。
型制約¶
extends
を使って、Type
型に特定のプロパティの存在を強制する。
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// 文字列や配列は `length` を持つので、エラーにならない。
const longerArray = longest("hogehoge", [1, 2, 3]);
// 数値は `length` を持たないので、エラーになる。
const notOK = longest(10, 100);
型引数の指定¶
以下のように配列と配列を結合する処理を書いたとする。
仮に別々の型を宣言した場合、エラーになる。
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
// 型が違うためエラー
const arr = combine([1, 2, 3], ["hello"]);
そのため、以下のようにしてエラーを回避することができる。
const arr = combine<string | number>([1, 2, 3], ["hello"]);
関数オーバーロード¶
柔軟すぎる引数を許容した関数を、ある程度まで絞り込む為の記述方法
// オーバーロード関数(2つ)
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
// 元の関数: 引数が多すぎる
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date { ...}
この場合、2 つのオーバーロード関数のみが呼び出し可能になり、
元の関数は呼び出せなくなる。
Note
可能な場合は、オーバーロードではなく、常にユニオン型のパラメータを優先します。
Tip
シグネチャ
とは、関数の「呼び出し方法」を示す設計図
具体例: function 関数名(引数: 型, 引数: 型): 戻り値の型
残余パラメータと引数¶
残りのパラメータ¶
関数では残余パラメーターを使用して無制限の数の引数を受け入れる関数を定義することもできる。
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
const a = multiply(10, 1, 2, 3, 4);
残り引数¶
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
// [ 1, 2, 3, 4, 5, 6 ]
パラメータ分解¶
パラメータ分割を使用すると、引数として渡されたオブジェクトを関数本体内の 1 つ以上のローカル変数に展開することができる。
function sum({ a, b, c }) {
console.log(a + b + c);
}
// それぞれの引数に値を割り当てることができる。
sum({ a: 10, b: 3, c: 9 });
// 型注釈ver
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
// React等でよく見る書き方
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
インデックスシグネチャ¶
「どんなキー名でも使えるオブジェクト」の型を定義する方法
interface MyObject {
[キー名: キーの型]: 値の型;
}
辞書型での使用が典型的。
type indexSignature = {
[key: string]: string;
sigName: string;
};
const dict: indexSignature = {
a: "hoge",
b: "hoge",
// この場合、1 は文字列として認識されるのでOK
1: "hoge",
// 別途定義した変数名は勿論OK
sigName: "hoge",
// 予め定義されていない変数名でもOK
sigNamea: "hoge",
};
console.log(dict);
// {
// '1': 'hoge',
// a: 'hoge',
// b: 'hoge',
// sigName: 'hoge',
// sigNamea: 'hoge'
// }
Input などのユーティリティコンポーネントを独自で使う際にも、プロパティを限定するのは難しいし冗長だと思えるため、そこで使用できるかもしれない。
例: [key: string]: any
汎用オブジェクト型¶
いろんな型に使い回せるオブジェクトの型
// 汎用ではない(特定用途のみ)
interface StringBox {
contents: string;
} // 文字列専用
interface NumberBox {
contents: number;
} // 数値専用
// 汎用オブジェクト型(どんな型でも使える)
interface Box<T> {
contents: T;
} // 何でも入る箱
// 使い方
const stringBox: Box<string> = { contents: "文字列" };
const numberBox: Box<number> = { contents: 123 };
const userBox: Box<User> = { contents: { name: "太郎" } };
タプル型¶
含まれる要素の数と、特定の位置にどの型が含まれているかを正確に把握している別の種類の型
type StringNumberPair = [string, number];
Note
タプル型は、各要素の意味が「明白」な、規約に厳密に準拠したAPIで役立ちます。
タプルを以下のように分解することも可能
function doSomething(stringHash: [string, number]) {
const [inputString, hash] = stringHash;
}
タプルには残りの要素も含めることができるが、残りの要素は配列/タプル型である必要がある。
要素名に?
を入れてオプションにすることも可能。
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
type Either2dOr3d = [number, number, number?];
ジェネリッククラス¶
class GenericNumber<NumType> {
// ここの型はインスタンス化時に決まる
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 1;
// 型が同じなので以下のように関数を宣言できる
myGenericNumber.add = function (x, y) {
return x + y;
};
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 10));
// 11
Note
型パラメータ = ジェネリクスで使う「型の変数」
クラス型の使用¶
function create<Type>(c: { new (): Type }): Type {
return new c();
}
createInstance(Lion).keeper.nametag;
keypf 型演算子¶
keyof
演算子はオブジェクト型を受取り、そのキーの和集合を表す。
type Point = { x: number; y: number };
type P = keyof Point;
// type P = "x" | "y":
インデックスアクセスタイプ¶
別の Type の特定のキーの型を参照することができる。
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
// `age` は `number` 型なので、「type Age = number」と同じ意味になる。
type I1 = Person["age" | "name"];
// 複数指定したい場合は上記
条件付き型制約¶
type Example = Dog extends Animal ? number : string;
// `Dog` が `Animal` を継承しているなら `number` 型になる
以下のように高度に分岐させることもできる。
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
// MessageOf<T> が `message` プロパティを持っていたら `message` 型を返し、
// 持っていなかったら `never` 型を返す。
type Flatten<T> = T extends any[] ? T[number] : T;
// 配列型なら平坦にし、そうでなかったらそのまま返す
この Flatten
で配列を渡したとき、要素が特定の型かどうかを調べるためには
infar
を使って以下を使う。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
分散条件型¶
以下の StrArrOrNumArr
のようにユニオン型を与えると分散型になる。
このままだと hoge
のように配列を宣言すると、エラーになる。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]
const hoge: StrArrOrNumArr = ["a", 1, 2, "b"];
// * Error
その場合は extends
の両側を角括弧でくくれば解消する。
type ToArray<Type> = [Type] extends [any] ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = (string | number)[]
const hoge: StrArrOrNumArr = ["a", 1, 2, "b"];
マッピング¶
特定の Type のプロパティのそれぞれの型を boolean で宣言したい場合等に使用する
type OptionsFlags<T> = {
[U in keyof T]: boolean;
};
// [ ]の中ではforループが走っているイメージ。
// `U` は任意の変数。
// T型のプロパティを全て `boolean`型で宣言し直した型が出来上がった。
readonly
を取り除く -
¶
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// type UnlockedAccount = {
// id: string;
// name: string;
// }
オプションを一括変換する +
, -
¶
マッピングした角括弧の後ろに修飾子をつけると以下の効果が得られる。
?
,+?
: プロパティ全てがオプションになる-?
: プロパティ全てが必須となる。
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
// type User = {
// id: string;
// name: string;
// age: number;
// }
キーの再マッピング as
¶
特定の Type のプロパティ名をそれぞれ一括で変更したい場合などに使う。
interface Person {
name: string;
age: number;
location: string;
}
type MappedNewProperties<Type> = {
[U in keyof Type as `test${Capitalize<string & U>}`]: Type[U];
};
type TestPerson = MappedNewProperties<Person>;
// type TestPerson = {
// testName: string;
// testAge: number;
// testLocation: string;
// }
Tip
Capitalize<T>
とは: 文字列の最初の文字を大文字にする組み込みのユーティリティ型
例) type Example1 = Capitalize<"hello">;
// "Hello"
特定のプロパティを除外して再マッピング¶
type RemoveKindField<Type> = {
// `kind` のプロパティを除外
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property];
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// type KindlessCircle = {
// radius: number;
// }
Tip
Exclude<T, U>
とは: Union型から特定の型を除外する組み込みユーティリティ型
T
から該当する U
を排除。
例) type Example1 = Exclude<"a" | "b" | "c", "a">;
// "b" | "c"
makeWatchedObject 関数 ()¶
プロパティの変更をトリガーとしてイベントを発火するための便利な関数としてmakeWatchedObject
がある。
通常以下のように使用する。
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
// ここで `on` を使い、引数に `{プロパティ名}Change` とすると
// プロパティの変更を監視することができる。
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});
.on
を使う際は文字列で渡す必要があるので、typo が発生しうる。
かと言って以下のように書くのは無駄。
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
on(
eventName:
"firstNameChanged" |
"lastNameChanged" |
"ageChanged",
callback: Function
): void;
});
そのため、以下のようにテンプレートリテラルを使用してスッキリさせる。
type PropEventSource<Type> = {
on(
eventName: `${string & keyof Type}Changed`,
callback: (newValue: any) => void
): void;
};
// ジェネリックで受け取った型に`Changed`を加えた文字列しか受け取らないようにする。
declare function makeWatchedObject<Type>(
obj: Type
): Type & PropEventSource<Type>;
// 改めて型定義をする。
Tip
declare
: 「実装は他の場所にあります。ここでは型情報だけ提供します」というもの
これによって、間違ったプロパティが指定されたときにエラーが発生する。
person.on("firstNameChanged", () => {});
// OK!
person.on("firstName", () => {});
// * Error
コールバック関数で以下のように引数を指定することもできる。
// `newName` は `on` 関数の引数。つまり、変更した文字列。
person.on("firstNameChanged", (newName) => {
console.log(`new name is ${newName.toUpperCase()}`);
});
// この例では `firstName` がstring型だから、その引数もstring型だと推論してくれている。
組み込み文字列操作型¶
型の文字列を操作する為の、型専用の toUpperCase()
のような書き方がある。
type Greeting = "hello, World";
type ShoutyGreeting = Uppercase<Greeting>;
// type ShoutyGreeting = "HELLO, WORLD"
type QuietGreeting = Lowercase<Greeting>;
// type QuietGreeting = "hello, world"
type CapitalizeGreeting = Capitalize<Greeting>;
// type Greeting = "Hello, world"
クラス¶
基本¶
以下のように宣言して、インスタンス化して利用する。
class Point {
x: number = 1;
y: number = 2;
}
const pt = new Point();
ただし、--strictPropertyInitialization
オプションなどで、明示的にコンストラクタを作成しなくてはいけない場合には
以下のようにconstructor()
を使用して記述する。
class GoodGreeter {
name: string;
constructor() {
// `this` をつける
this.name = "hello";
}
}
implements¶
クラス設計が正しいかを確認する記述。
interface Pingable {
ping(): void;
}
// `implements` を使って、`Pingable` に型が準拠しているかを確認
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
class Ball implements Pingable {
// ※ typo検出。`Pingable` には `pong` というメソッドはないので
// ここでエラーが吐かれる
pong() {
console.log("pong!");
}
}
Tip
クラスは複数のインターフェースを実装することもできます 例: class C implements A, B {)
Note
implements
はあくまで型チェック。継承の extends
とは用途が異なる。
extends¶
クラスは基底クラスから派生する。
派生クラスは基底クラスのすべてのプロパティとメソッドを持ち、追加のメンバーを定義することもできる。
オーバーライド¶
super()
を使用して基底クラスのプロパティにアクセスすることができる。
class Base {
greet() {
console.log("Hello, world!");
}
}
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
// 基底クラスの `greet` メソッドが呼び出される。
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
protected¶
protected
は、それが宣言されているサブクラス飲みでしか使用されない
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Tip
子クラスでは再宣言してpublic
にすることも可能。
private¶
サブクラスからでもメンバーへのアクセスは許可されない。
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
// Property 'x' is private and only accessible within class 'Base'.
// ※角括弧でのアクセスは可能。ソフトプライベート
console.log(s["secretKey"]);
staticメンバ¶
インスタンス化不要でクラスのメンバにアクセスすることができる。
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
Note
pythonの @staticmethod
みたいな使用方法。
継承も出来る。
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
抽象クラス (abstract)¶
「未完成の設計図」のようなクラスで、直接は使えないが 継承 して完成させることができます。
abstract class Animal {
abstract makeSound(): string; // 実装なし(抽象メソッド)
// 通常のメソッド(実装あり)
introduce() {
console.log(`私は ${this.makeSound()} と鳴きます`);
}
}
// ❌ エラー:抽象クラスは直接インスタンス化できない
const animal = new Animal();
class Dog extends Animal {
makeSound(): string {
return "ワンワン"; // 抽象メソッドを実装
}
}
// ✅ OK:具象クラスはインスタンス化できる
const dog = new Dog();
dog.introduce(); // "私は ワンワン と鳴きます"
Note
インターフェースは「何をすべきか」、抽象クラスは「どう実装するか(の一部)」を提供
new () => Base¶
引数なしでnewして、Baseのインスタンスを返すコンストラクタ
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
// 以下のように呼び出す
greet(Base);