白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

TypeScriptの型を復習する その1

こんばんは。
急に寒くなってきましたね。体が寒いのに慣れていないっぽくて体調がイマイチです。

なんか型関連の見慣れない記法あるよね

TypeScriptはJavaScriptに型の概念を追加した言語です。
なので静的型付け言語に分類されるのですが、強力な型推論や構造的型付けによって動的な型導出のような、いわゆるネイティブな静的型付け言語とは少し違ったアプローチが取れます。
TypeScriptの型については既にいくつか記事を書いたことはあるのですが、TypeScriptをうまく活用するために大切そうなので改めて復習してみます。

shironeko.hateblo.jp
shironeko.hateblo.jp

typeof

いきなりややこしいのですが、TypeScriptの「typeof」は「typeof演算子」と「typeof型演算子」の2種類があります。
前者はJavaScriptの機能、後者はTypeScriptの型システムの機能です。
こんな感じの型とオブジェクトを用意しましょう。

type User = {
  name: string;
  age?: number;
};

const user : User = {
  name: "Alice",
  age: 30,
};
typeof演算子

typeof演算子JavaScriptの組み込み演算子で、オペランドの型を表す文字列を返します。
値を要求する場面ではtypeof演算子として型の文字列を返します。

test('typeof演算子', () => {
  expect(typeof user).toBe('object');
  expect(typeof user.name).toBe('string');
  expect(typeof user.age).toBe('number');
});

JavaScriptの仕様でnullはobjectと判定されるので注意が必要です。

test('typeof演算子(null)', () => {
  const value = null;
  expect(typeof value).toBe('object');
});
typeof型演算子

typeof型演算子はTypeScriptの型システムの一部で、オペランドの型を取得します。
型を要求する場面では型クエリとして型を返します。

test('typeof型演算子', () => {
  type UserType = typeof user;
  expectTypeOf<UserType>().toEqualTypeOf<{ name: string; age?: number }>();
});

null型も取得できますし、関数型とかも取れます。

test('typeof型演算子(null)', () => {
  const value = null;
  type ValueType = typeof value;
  expectTypeOf<ValueType>().toEqualTypeOf<null>();
});

test('typeof型演算子(関数)', () => {
  function greet(name: string): string {
    return `Hello, ${name}!`;
  }
  type GreetType = typeof greet;
  expectTypeOf<GreetType>().toEqualTypeOf<(name: string) => string>();
});
文脈の違い

あくまでどんな文脈で利用されるかなので、慣れないとちょっと難しいですね。

test('複合', () => {
  const user2: typeof user = {  // こっちの文脈では型を要求している
    name: typeof user,          // こっちの文脈では値を要求している
    age: 25,
  };
  expectTypeOf(user2).toEqualTypeOf<{ name: string; age?: number }>();
  expect(user2.name).toBe('object');
});

keyof

「keyof」はTypeScriptの型演算子で、オブジェクト型のすべてのキーのユニオン型を取得します。
プロパティが1つだけ場合はプロパティ名のリテラル型になります。

test('プロパティ1つ', () => {
  type User = {
    name: string;
  };
  type UserKeys = keyof User;
  expectTypeOf<UserKeys>().toEqualTypeOf<"name">();
});

プロパティが複数ある場合はプロパティ名のユニオン型になります。
ちなみにUserKeysをIDEで確認すると「type UserKeys = "name" | "age"」ではなく「type UserKeys = keyof User」という形で表示されることがありますが、同じ意味です。

test('プロパティ複数', () => {
  type User = {
    name: string;
    age: number;
  };
  type UserKeys = keyof User;
  expectTypeOf<UserKeys>().toEqualTypeOf<"name" | "age">();
});

プロパティがない場合はnever型になります。

test('プロパティがない', () => {
  type User = {};
  type UserKeys = keyof User;
  expectTypeOf<UserKeys>().toEqualTypeOf<never>();
});

要するにプロパティ名がすべて取得できるわけですが、あくまで型として取得されるということが重要です。
例えば関数の引数にkeyofで取得した型を使うことで、オブジェクトのプロパティ名だけを受け取る関数を定義できます。

test('関数の引数にkeyofで取得した型を利用', () => {
    type User = {
        name: string;
        age: number;
    };
    type UserKeys = keyof User;

    function getProperty(obj: User, key: UserKeys): string | number {
        return obj[key];
    }

    const user: User = {
        name: "Bob",
        age: 25,
    };
    const name = getProperty(user, "name");
    const age = getProperty(user, "age");
    // const invalid = getProperty(user, "invalid"); // エラー

    expect(name).toBe("Bob");
    expect(age).toBe(25);
});

とりあえず今回はこの2つ。
keyofはMapped Typesと一緒に使われることが多いですが、Mapped Typesに関してはまた次回。