0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptの型システムとクラス構文、JavaScriptとの差分を整理した

0
Posted at

この記事を書いた理由

JavaScriptはある程度書けるようになってきたが、TypeScriptに入門したとき「型をつけるだけじゃないんだな」という感覚があった。静的型付けの恩恵は理解しやすいが、アクセス修飾子やインターフェイス、ジェネリクスあたりの概念がしばらくぼんやりしたままだった。

JavaとPythonをある程度触ってきた立場で、TypeScriptの「JavaScriptをどう拡張しているか」という観点で整理してみた。


まず:TypeScriptはブラウザで直接動かない

最初に把握しておくべき前提として、TypeScriptはそのままブラウザで実行できない。.ts ファイルを tsc コマンドでコンパイルすると、JavaScriptファイルが生成される。

npm install -g typescript
tsc hello.ts

コンパイル時に型エラーが検出されるのがTypeScriptの核心で、たとえばこういうケースだ。

function greeter(person: string) {
    return "Hello, " + person;
}

greeter(10); // エラー
// Argument of type '10' is not assignable to parameter of type 'string'.

JavaScriptなら実行時まで気づけないミスが、コンパイル時に検出される。

学習中は TypeScript Playground を使うとブラウザ上で手軽に試せるし、コンパイル後のJavaScriptも確認できて理解しやすかった。


型の基本と型推論

TypeScriptの型宣言はシンプルで、変数名の後にコロンで型を指定する。

let data: string = 'hoge';
data = 100; // エラー

型を省略しても、初期値から推論してくれる。

let data = 100;  // number型と推論される
data = 'hoge';   // エラー

let data2;       // 初期値なし → any型とみなされる
data2 = 100;     // エラーにならない
data2 = 'hoge';  // エラーにならない

any 型は型チェックをスキップするので、乱用すると型安全の恩恵が失われる。初期値なしで宣言した変数が any になることは意識しておきたい。


知っておくと便利な型の種類

ユニオン型:複数の型のどれかを許容する。

let data: string | boolean;
data = 'hoge';
data = false;
data = 100; // エラー

文字列リテラル型:特定の文字列だけを許容する。APIのステータスやカテゴリの管理で実用的だと感じた。

type Fruit = 'apple' | 'orange' | 'peach';
let data: Fruit = 'apple';
data = 'grape'; // エラー

タプル型:異なる型を順番付きで持てる配列。

let data: [string, number, boolean] = ['hoge', 100, true];

関数の型定義

引数と返り値に型をつける。返り値がない場合は void を指定する。

function hello(name: string): string {
    return `${name}さん、こんにちは!`;
}

function log(message: string): void {
    console.log(message);
}

引数を省略可能にするには ? をつける。デフォルト値も設定できる。

function greet(name?: string): string {
    return name === undefined ? 'スルー' : `${name}さん、こんにちは!`;
}

function greet2(name: string = 'tom'): string {
    return `${name}さん、こんにちは!`;
}

可変長引数は ... を使う。

function hello(...names: string[]) {
    return names.map(n => `${n}さん`).join('');
}

クラスとアクセス修飾子

TypeScriptのクラスには3種類のアクセス修飾子がある。

  • public:クラスの外からアクセス可能(デフォルト)
  • protected:同じクラスと派生クラスからアクセス可能
  • private:同じクラスの中からのみアクセス可能
class Person {
    private name: string;
    private job: string;

    constructor(name: string, job: string) {
        this.name = name;
        this.job = job;
    }

    public introduce(): string {
        return `${this.name}${this.job}です`;
    }
}

const tom = new Person('tom', 'エンジニア');
console.log(tom.introduce()); // tomはエンジニアです
console.log(tom.name);        // エラー(privateのためアクセス不可)

コンストラクタの引数にアクセス修飾子を直接書くと、プロパティの宣言と代入を省略できる。

class Person {
    constructor(private name: string, private job: string) {}

    public introduce(): string {
        return `${this.name}${this.job}です`;
    }
}

抽象クラスとインターフェイスの違い

ここが最初に一番曖昧だった部分だ。

抽象クラス は、派生クラスでの実装を強制したいメソッドを abstract で定義できる。ただし複数の抽象クラスを同時に継承はできない(単一継承)。

abstract class Figure {
    constructor(protected width: number, protected height: number) {}
    abstract getArea(): number;
}

class Triangle extends Figure {
    getArea(): number {
        return this.width * this.height / 2;
    }
}

インターフェイス は、すべてのメソッドが抽象メソッドである特別な型の定義だ。複数のインターフェイスを同時に実装できる点が抽象クラスとの大きな違いになる。

interface Printable {
    print(): void;
}

interface Serializable {
    serialize(): string;
}

class Document implements Printable, Serializable {
    print(): void { console.log('printing...'); }
    serialize(): string { return JSON.stringify(this); }
}

整理すると、「実装の一部を共有したい」なら抽象クラス、「型の契約だけ定義したい」ならインターフェイスという判断になる。

余談だが、JavaやC#でインターフェイスを書いてきた人にとっては馴染みやすい概念だと思う。ただJavaScriptしか触ったことがない状態でいきなりここに来ると、なぜわざわざこれを使うのかピンとこないかもしれない。


ジェネリクス

汎用的なクラスやメソッドに、使う時点で型を指定できる仕組みだ。

class MyBox<T> {
    value: T;
    getValue(): T {
        return this.value;
    }
}

const box = new MyBox<string>();
box.value = 'hello';
console.log(box.getValue()); // hello

T という型パラメータは慣習的な名前で、実際に使う際に stringnumber などの具体的な型を指定する。同じクラスを複数の型で再利用できるので、汎用的なコレクションやユーティリティを書くときに活きてくる。


振り返って

今回整理できたのは、型システムの基本的な種類・関数への型付け・クラスとアクセス修飾子・抽象クラスとインターフェイスの使い分け・ジェネリクスの入り口あたりだ。

まだ整理できていないのは、型アサーションの適切な使いどころと、unknown 型と any 型の具体的な使い分けの基準だ。「any は使うな、unknown を使え」という話はよく見るが、実際にどういう場面で unknown が力を発揮するのかを手を動かして確かめていない。そこは引き続き整理したい。


この記事を書いた人について

株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。

技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?