💡

TypeScript型道場 2024!

2024/12/23に公開

みなさまこんにちは!エアークローゼットの大西です。この記事はエアークローゼットのアドベントカレンダー2024の23日目の記事となってます。

今回はクイズ形式で型について学べる記事を考えてみました!

初級、中級、上級をそれぞれ2問ずつ用意しています!
ただ、知識を問うだけでは面白くないので、ちょっと頭を使う内容にしてみました。(結果的にジェネリクス型の問題ばかりになりました笑)

レベルは主観でつけているので参考程度に見てください。
それではみなさま楽しみながら学んでいきましょう!

初級

問題1

次の関数getPropertyは、オブジェクトから指定されたキーの値を取得します。関数の第一引数にオブジェクト型、第二引数に第一引数に存在するキーのみを許容する型を定義してください。

const getProperty = </* ジェネリクス型を定義 */>(
  obj: /* 型を定義 */,
  key: /* 型を定義 */, 
): /* 戻り値の型を定義 */ => {
  return obj[key];
};

const person = { name: 'Bob', age: 40, location: 'New York' };
const personName = getProperty(person, 'name'); // string
const personAge = getProperty(person, 'age'); // number
// const invalidProperty = getProperty(person, "email"); // エラーになるようにする
解答&解説

TypeScriptのジェネリクス型とキー制約(keyof)を組み合わせて、関数がオブジェクトの有効なプロパティキーのみを受け取るようにします。ジェネリクス型Tを用いて任意のオブジェクト型を受け取り、K extends keyof Tとすることで、KがTのプロパティキーに限定されます。

const getProperty = <T, K extends keyof T>(obj: T, key: K): T[K] => {
  return obj[key];
};

const person = { name: 'Bob', age: 40, location: 'New York' };
const personName = getProperty(person, 'name'); // string
const personAge = getProperty(person, 'age'); // number
// const invalidProperty = getProperty(person, "email"); // エラー: Argument of type '"email"' is not assignable to parameter of type '"name" | "age" | "location"'.

問題2

次のインターフェースPointは、x座標とy座標を持ちます。これらのプロパティを読み取り専用に設定してください。

interface Point {
    /* 型を定義 */
}

const point: Point = { x: 10, y: 20 };
point.x = 15; // エラーになるようにする
解答&解説

readonly修飾子を使用してプロパティを読み取り専用にします。これにより、プロパティの値を変更しようとするとコンパイル時にエラーが発生します。

interface Point {
    readonly x: number;
    readonly y: number;
}

const point: Point = { x: 10, y: 20 };
point.x = 15; // エラー: Cannot assign to 'x' because it is a read-only property.

中級

問題1

TypeScriptのユーティリティ型Record<Keys, Type>をRecordを使わずに実装してください。

type MyRecord</* ジェネリクス型を定義 */> = /* 実装を記述 */

type Roles = "admin" | "user" | "guest";

type RolePermissions = MyRecord<Roles, string[]>;

// RolePermissionsは以下のようになる
// {
//     admin: string[];
//     user: string[];
//     guest: string[];
// }
解答&解説

{ [P in K]: V }により、指定されたキーKすべてに対して、型Vの値を持つプロパティを生成します。

type MyRecord<K extends keyof any, V> = {
    [P in K]: V;
};

type Roles = "admin" | "user" | "guest";

type RolePermissions = MyRecord<Roles, string[]>;
// RolePermissionsは以下のようになる
// {
//     admin: string[];
//     user: string[];
//     guest: string[];
// }

問題2

次の型ExtractProperties<T, U>は、Tがオブジェクト型、Uが任意の型であり、TのプロパティのうちUにマッチするもののみを抽出します。ExtractProperties<T, U>を実装してください。

type ExtractProperties<T, U> = /* 実装を記述 */

interface Employee {
    id: number;
    name: string;
    salary: number;
    department: string;
}

// NumericPropertiesは { id: number; salary: number; } 型となる
type NumericProperties = ExtractProperties<Employee, number>;
解答&解説

[K in keyof T as T[K] extends U ? K : never]とすることで、型Tの各プロパティKについて、T[K]が型Uにマッチする場合のみプロパティKを保持します。そうでない場合はneverとなり、そのプロパティは除外されます。

type ExtractProperties<T, U> = {
    [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Employee {
    id: number;
    name: string;
    salary: number;
    department: string;
}

// NumericPropertiesは { id: number; salary: number; } 型となる
type NumericProperties = ExtractProperties<Employee, number>;

上級

問題1

次の型MutableDeep<T>は、型Tの全てのプロパティを再帰的に可変にします(readonlyを解除します)。既存のユーティリティ型を拡張し、MutableDeep<T>を実装してください。

type MutableDeep<T> = /* 実装を記述 */

interface DeepReadonlyUser {
    readonly id: number;
    readonly name: string;
    readonly address: {
        readonly street: string;
        readonly city: string;
    };
}

// MutableUserは { id: number; name: string; address: { street: string; city: string; } } 型となる
type MutableUser = MutableDeep<DeepReadonlyUser>;

const user: MutableUser = {
  id: 1,
  name: 'Dhik',
  address: {
    street: "表参道",
    city: "港区"
  }
}
user.name = "Ryan"; // エラーが出ない
user.address.city = "六本木"; // エラーが出ない

解答&解説

ポイントは以下の2点です。

  • [K in keyof T]: MutableDeep<T[K]>で値がオブジェクトだった場合、再帰的に処理を行います。
  • -readonlyにより、全てのプロパティのreadonly修飾子を解除します
type MutableDeep<T> = T extends object
  ? { -readonly [K in keyof T]: MutableDeep<T[K]> }
  : T;

interface DeepReadonlyUser {
  readonly id: number;
  readonly name: string;
  readonly address: {
    readonly street: string;
    readonly city: string;
  };
}

// MutableUserは { id: number; name: string; address: { street: string; city: string; } } 型となる
type MutableUser = MutableDeep<DeepReadonlyUser>;

const user: MutableUser = {
  id: 1,
  name: 'Dhik',
  address: {
    street: '表参道',
    city: '港区',
  },
};
user.name = 'Ryan'; // エラーが出ない
user.address.city = '六本木'; // エラーが出ない

問題2

入力された型がPromise型であればPromiseを解決した型を、そうでなければ入力した型自身を返す型AwaitedTypeを実装してください。
ちなみに、ほぼ同じ効果のユーティリティ型にTypeScript 4.5で実装されたAwaitedがあります。こちらは再帰的にPromiseを解決してくれます。便利ですね!

type AwaitedType</* ジェネリクス型を定義 */> = /* 実装を記述 */

type PromiseResult = AwaitedType<Promise<string>>; // string
type NonPromise = AwaitedType<number>; // number
type NestedPromise = AwaitedType<Promise<Promise<boolean>>>; // Promise<boolean>
解答&解説

T extends Promise<infer U>によりTがPromise<U>型であるかを判定し、Uを抽出します。

type AwaitedType<T> = T extends Promise<infer U> ? U : T;

type PromiseResult = AwaitedType<Promise<string>>; // string
type NonPromise = AwaitedType<number>; // number
type NestedPromise = AwaitedType<Promise<Promise<boolean>>>; // Promise<boolean>

最後に

今回出した問題はChatGPTに作ってもらいました。
本当に便利な世の中になりましたね!
(読みやすくするためにそれなりの手直しをしたので、文章の読みやすさまで意識した文章を出してくれると嬉しいのですが。。。)

また、エアークローゼットはエンジニア採用活動も行っておりますので、興味のある方はぜひご覧ください!
https://corp.air-closet.com/recruiting/developers/

https://www.wantedly.com/companies/airCloset/projects

ここまで読んでいただきありがとうございました!
エアークローゼットのアドベントカレンダーも残り2日となりました。
残りの今後の記事もお楽しみにしてください!

Discussion