TypeScriptの型演習

この記事は、TypeScriptの型を使いこなすための演習として、TypeScriptの型に関する練習問題を提供する記事です。解いて自分のTypeScript力を確かめてみましょう。

問題のレベルは、筆者の既存記事「[TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)」「[TypeScriptの型初級](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f)」を完全に理解した人なら全部解けるという想定で作られています。記事を読んでいない人が腕試しに解いてみるのも問題ありません。また、記事を読んだけど全部は理解していないという場合でもご安心ください。解ける問題はありますから、ぜひ挑戦してみましょう。

問題は20問あり、4段階の難易度別に分かれています。同じ難易度帯の問題は思いついた順で並んでいるので、後のほうが難しいわけではありません。問題は執筆時点の最新版のTypeScript（TypeScript 3.3.3333）で`--strict`オプションありの状態で動作を確認しています。

問題の解答・解説は[TypeScriptの型演習（解答・解説編）](https://qiita.com/uhyo/items/0e7821ce494024c98da5)を参照してください。

## 難易度：★☆☆☆　基礎の基礎レベル
基本的な型が分かっていれば解ける問題たちです。

### 1-1. 関数に型をつけよう

次の関数`isPositive`は、数値を受け取ってそれが0以上なら`true`、0未満なら`false`を返す関数です。この関数に正しく型アノテーションを付けてください。型アノテーションとは、引数や返り値の型をソースコード中に明示することです。

```ts
function isPositive(num) {
    return num >= 0;
}

// 使用例
isPositive(3);

// エラー例
isPositive('123');
const numVar: number = isPositive(-5);
```

### 1-2. オブジェクトの型

1人のユーザーのデータを表すオブジェクトは、`name`プロパティと`age`プロパティ、そして`private`プロパティを持っています。`name`は文字列、`age`は数値、`private`は真偽値です。ユーザーデータのオブジェクトの型`User`を定義してください。

```ts
function showUserInfo(user: User) {
    // 省略
}

// 使用例
showUserInfo({
    name: 'John Smith',
    age: 16,
    private: false,
});

// エラー例
showUserInfo({
    name: 'Mary Sue',
    private: false,
});
const usr: User = {
    name: 'Gombe Nanashino',
    age: 100,
};
```

### 1-3. 関数の型

以下のコードで定義される関数`isPositive`は、数値を受け取ってその数値が0以上なら`true`を、0未満なら`false`を返す関数です。以下のコードに合うように適切な型`IsPositiveFunc`を定義してください。

```ts
const isPositive: IsPositiveFunc = num => num >= 0;

// 使用例
isPositive(5);

// エラー例
isPositive('foo');
const res: number = isPositive(123);
```

### 1-4. 配列の型

以下のコードで定義される関数`sumOfPos`は、数値の配列を受け取って、そのうち0以上の値の和を返す関数です。適切な型アノテーションをつけてください。

```ts
function sumOfPos(arr) {
  return arr.filter(num => num >= 0).reduce((acc, num) => acc + num, 0);
}

// 使用例
const sum: number = sumOfPos([1, 3, -2, 0]);

// エラー例
sumOfPos(123, 456);
sumOfPos([123, "foobar"]);
```

## 難易度：★★☆☆　基本レベル
よく使う機能を一通り知っていれば解ける問題たちです。

### 2-1. ジェネリクス

以下のコードで定義される関数`myFilter`は、配列の`filter`関数を再実装したものです。`myFilter`関数に適切な型アノテーションを付けてください。

`myFilter`関数は色々な型の配列を受け取れる点に注意してください。必要に応じて`myFilter`に型引数を追加しても構いません。

```ts
function myFilter(arr, predicate) {
  const result = [];
  for (const elm of arr) {
    if (predicate(elm)) {
      result.push(elm);
    }
  }
  return result;
}

// 使用例
const res = myFilter([1, 2, 3, 4, 5], num => num % 2 === 0);
const res2 = myFilter(['foo', 'hoge', 'bar'], str => str.length >= 4);

// エラー例
myFilter([1, 2, 3, 4, 5], str => str.length >= 4);
```

### 2-2. いくつかの文字列を受け取れる関数

以下のコードで定義される`getSpeed`は、`'slow'`, `'medium'`, `'fast'`のいずれかの文字列を受け取って数値を返す関数です。この関数に他の文字列を渡すのは型エラーとしたいです。この条件を満たすように型`Speed`を定義してください。

```ts
type Speed = /* ここを入力 */;

function getSpeed(speed: Speed): number {
  switch (speed) {
    case "slow":
      return 10;
    case "medium":
      return 50;
    case "fast":
      return 200;
  }
}

// 使用例
const slowSpeed = getSpeed("slow");
const mediumSpeed = getSpeed("medium");
const fastSpeed = getSpeed("fast");

// エラー例
getSpeed("veryfast");
```
### 2-3. 省略可能なプロパティ

[EventTarget#addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener)は、2つまたは3つの引数を受け取る関数で、返り値はありません。1つ目の引数は文字列、2つ目の引数は関数です。そして3つ目の引数は省略可能であり、真偽値またはオブジェクトを渡すことができます。オブジェクトに存在可能なプロパティは`capture`, `once`, `passive`の3つで、全て真偽値であり、省略可能です。

このようなインターフェースを持つ関数`addEventListener`を`declare`を用いて宣言してください。簡単のために、第2引数に指定する関数は引数無しで何も返さない関数としてください。

```ts
// 使用例
addEventListener("foobar", () => {});
addEventListener("event", () => {}, true);
addEventListener("event2", () => {}, {});
addEventListener("event3", () => {}, {
  capture: true,
  once: false
});

// エラー例
addEventListener("foobar", () => {}, "string");
addEventListener("hoge", () => {}, {
  capture: true,
  once: false,
  excess: true
});
```

なお、`declare`はTypeScriptに特有の構文であり、以下のように関数や変数の型を中身なしに宣言できる構文です。

```ts:declareの例
declare function foo(arg: number): number;
```

### 2-4. プロパティを1つ増やす関数
下のコードで定義される`giveId`関数は、オブジェクトを受け取って、それに新しい文字列型のプロパティ`id`を足してできる新しいオブジェクトを返す関数です。この関数に適切な型を付けてください。なお、簡単のために、`giveId`に渡されるオブジェクト`obj`が既に`id`プロパティを持っている場合は考えなくて構いません。

```ts
function giveId(obj) {
  const id = "本当はランダムがいいけどここではただの文字列";
  return {
    ...obj,
    id
  };
}

// 使用例
const obj1: {
  id: string;
  foo: number;
} = giveId({ foo: 123 });
const obj2: {
  id: string;
  num: number;
  hoge: boolean;
} = giveId({
  num: 0,
  hoge: true
});

// エラー例
const obj3: {
  id: string;
  piyo: string;
} = giveId({
  foo: "bar"
});
```

### 2-5. useState

Reactの`useState`関数は、ステートを宣言するために使われます。引数は1つで、宣言されるステートの初期値です。返り値は2つの要素を持つ配列で、最初の要素は現在のステートの値です。2つ目の要素は関数であり、呼び出すとステートを更新できます。ステート更新関数は引数に新しいステートの値を受け取ることができるほか、現在の値を受け取って新しい値を返す関数を渡すことができます。`useState`の使い方は以下の使用例も参考にしてください。

このような`useState`を`declare`で宣言してください。ただし、`useState`はステートの値の型を型引数として取るようにしてください。

```ts
// 使用例
// number型のステートを宣言 (numStateはnumber型)
const [numState, setNumState] = useState(0);
// setNumStateは新しい値で呼び出せる
setNumState(3);
// setNumStateは古いステートを新しいステートに変換する関数を渡すこともできる
setNumState(state => state + 10);

// 型引数を明示することも可能
const [anotherState, setAnotherState] = useState<number | null>(null);
setAnotherState(100);

// エラー例
setNumState('foobar');
```

## 難易度：★★★☆　脱入門レベル
[TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)の内容をとりあえず知っていれば解ける問題たちです。

関数に適切な型を付ける問題では、型引数を自由に足して構いません。また、引数や返り値の型にアノテーションを施してもTypeScriptの型推論能力が足りずに関数内で型エラーが発生することがあります。その場合は`as`などを用いて適宜エラーを回避しても構いません。

### 3-1. 配列からMapを作る
以下のコードで定義される関数`mapFromArray`は、オブジェクトの配列から`Map`を作って返す関数です。配列から取り出した各オブジェクトを`Map`に登録しますが、その際にキーとして各オブジェクトの指定されたプロパティの値を用います。`mapFromArray`に適切な型を付けてください。

```ts
function mapFromArray(arr, key) {
  const result = new Map();
  for (const obj of arr) {
    result.set(obj[key], obj);
  }
  return result;
}

// 使用例
const data = [
  { id: 1, name: "John Smith" },
  { id: 2, name: "Mary Sue" },
  { id: 100, name: "Taro Yamada" }
];
const dataMap = mapFromArray(data, "id");
/*
dataMapは
Map {
  1 => { id: 1, name: 'John Smith' },
  2 => { id: 2, name: 'Mary Sue' },
  100 => { id: 100, name: 'Taro Yamada' }
}
というMapになる
*/

// エラー例
mapFromArray(data, "age");
```

### 3-2. Partial
`Partial`はTypeScriptの標準ライブラリに定義されている型で、オブジェクトの型を渡されると、その各プロパティを全部省略可能にするものです。`MyPartial`という名前でこれを実装してください。

```ts
// 使用例
/*
 * T1は { foo?: number; bar?: string; } となる
 */
type T1 = MyPartial<{
  foo: number;
  bar: string;
}>;
/*
 * T2は { hoge?: { piyo: number; } } となる
 */
type T2 = MyPartial<{
  hoge: {
    piyo: number;
  };
}>;
```

### 3-3. イベント
以下のコードで定義される`EventDischarger`クラスは、`emit`メソッドを呼び出すことでイベントを発生させる機能を持っています。イベントは`"start"`, `"stop"`, `"end"`の3種類であり、それぞれのイベントを発生させるときはイベントに合ったデータを`emit`メソッドに渡す必要があります。具体的には、`"start"`イベントに対しては`{ user: string }`型のデータを、`"stop"`イベントに対しては`{ user: string; after: number }`型のデータを、そして`"end"`イベントに対しては`{}`型のデータを渡さなくてはなりません。各イベント名に対して必要なデータの型は、`EventPayloads`型に`イベント名: データの型`の形でまとめて定義してあります。

いま、`emit`メソッドが間違ったイベント名やデータに対しては型エラーを出すようにしたいです。`emit`メソッドに適切な型を付けてください。ただし、`EventDischarger`の汎用性を上げるために、`EventDischarger`はイベントを定義する型である`EventPayloads`を型引数`E`として受け取るようになっています。`emit`は、`E`に定義されたイベントを適切に受け付ける必要があります。

```ts
interface EventPayloads {
  start: {
    user: string;
  };
  stop: {
    user: string;
    after: number;
  };
  end: {};
}

class EventDischarger<E> {
  emit(eventName, payload) {
    // 省略
  }
}

// 使用例
const ed = new EventDischarger<EventPayloads>();
ed.emit("start", {
  user: "user1"
});
ed.emit("stop", {
  user: "user1",
  after: 3
});
ed.emit("end", {});

// エラー例
ed.emit("start", {
  user: "user2",
  after: 0
});
ed.emit("stop", {
  user: "user2"
});
ed.emit("foobar", {
  foo: 123
});
```

### 3-4. reducer
以下のコードで定義される関数`reducer`は、現在の数値とアクションを受け取って、それに応じて新しい数値を返す関数です。アクションは3種類あり、加算を表すアクションは`{ type: "increment", amount: 数値 }`という形のオブジェクトです。減算を表すアクションは`{ type: "decrement", amount: 数値 }`という形です。数値のリセットを表すアクションは`{ type: "reset", value: 数値 }`という形です。`reducer`に適切な型をつけてください。

```ts
const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return state + action.amount;
    case "decrement":
      return state - action.amount;
    case "reset":
      return action.value;
  }
};

// 使用例
reducer(100, {
    type: 'increment',
    amount: 10,
}) === 110;
reducer(100, {
    type: 'decrement',
    amount: 55,
}) === 45;
reducer(500, {
    type: 'reset',
    value: 0,
}) === 0;

// エラー例
reducer(0,{
    type: 'increment',
    value: 100,
});
```

### 3-5. undefinedな引数
以下のコードで定義されている型`Func<A, R>`は、`A`型の引数をひとつ受け取って`R`型の値を返すような関数の型です。JavaScriptでは与えられなかった引数は`undefined`が入ることが知られていますから、`f2`のように`A`が`undefined`型のときは引数無しで呼べるようにしたいです。一応、`v3`のように明示的に`undefined`を渡して呼び出すのもOKとしたいです。一方、`v4`のように、引数が`undefined`以外のときは引数の省略は許しません。

以上の動作をするように、型`Func<A, R>`を定義しなおしてください。

```ts
type Func<A, R> = (arg: A) => R;

// 使用例
const f1: Func<number, number> = num => num + 10;
const v1: number = f1(10);

const f2: Func<undefined, number> = () => 0;
const v2: number = f2();
const v3: number = f2(undefined);

const f3: Func<number | undefined, number> = num => (num || 0) + 10;
const v4: number = f3(123);
const v5: number = f3();

// エラー例
const v6: number = f1();
```

## 難易度：★★★★　初心者卒業レベル
応用的な内容や[TypeScriptの型初級](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f)の内容を含む問題たちです。解答にはTypeScriptの標準ライブラリで定義されている型（`Record`、`Partial`など）を使用しても構いません。また、これまでと同様に適宜型引数を用いたり`as`を使用したりしても構いません。

### 4-1. 無い場合はunknown
以下のコードで定義される`getFoo`は、与えられたオブジェクトの`foo`プロパティを返す関数です。この関数に適切な型を付けてください。

ただし、`foo`プロパティが`string`型を持つオブジェクトを渡されたら`getFoo`の返り値の型が`string`になる、というように、引数に応じて`getFoo`の型が適切に変化するようにしてください。また、`foo`プロパティを持たないオブジェクトを渡された場合は、エラーではなく返り値の型が`unknown`となるようにしてください。さらに、`123`や`null`など、オブジェクトでない値を渡すのは型エラーとなるようにしてください。

```ts
function getFoo(obj) {
  return obj.foo;
}

// 使用例
// numはnumber型
const num = getFoo({
  foo: 123
});
// strはstring型
const str = getFoo({
  foo: "hoge",
  bar: 0
});
// unkはunknown型
const unk = getFoo({
  hoge: true
});

// エラー例
getFoo(123);
getFoo(null);
```

### 4-2. プロパティを上書きする関数
以下の`giveId`関数は問題2-4と同じもので、渡されたオブジェクト`obj`にプロパティ`id`を加えてできる新しいオブジェクトを返す関数です。問題2-4では簡単のために`obj`が既に`id`を持っている場合は考えませんでしたが、今回はそのような場合も考えることにします。

`obj`が既に`id`プロパティを持っている場合、`giveId`は`id`プロパティを上書きします。例えば、`giveId({foo: 123, id: 456})`は`{foo: 123, id: '本当は（略'}`というオブジェクトになります。このことを加味して、`giveId`に適切な型をつけてください。なお、問題2-4の想定解では一番下の`obj2.id = '';`がエラーになりますが、今回はこれがエラーにならないようにする必要があります。

```ts
function giveId(obj) {
  const id = "本当はランダムがいいけどここではただの文字列";
  return {
    ...obj,
    id
  };
}

// 使用例
/*
 * obj1の型は { foo: number; id: string } 型
 */
const obj1 = giveId({ foo: 123 });
/*
 * obj2の型は { num : number; id: string } 型
 */
const obj2 = giveId({
  num: 0,
  id: 100,
});
// obj2のidはstring型なので別の文字列を代入できる
obj2.id = '';
```

### 4-3. unionは嫌だ
以下のコードで定義される`EventDischarger`は問題3-3と同じものです。実は、問題3-3で想定される解答にはひとつ問題があります。それは、コード一番下の「エラー例」にあるように`emit`の型引数に`"start" | "stop"`のようなユニオン型を渡すことで型チェックを欺いて不正なデータを渡すことができる点です。この例ではイベント名が`"stop"`なのにデータは`"start"`用のものになっています。

このような問題を回避するために、`emit`の型引数`Ev`をイベント名のユニオン型にして関数が呼ばれるのを型エラーで防ぐことにしました。この要件を満たすように`emit`の型を完成させてください。

**ヒント**（白い文字で書かれています）：　<font color="white">Evがユニオン型のときはpayloadの型をneverにしましょう。</font>

```ts
interface EventPayloads {
  start: {
    user: string;
  };
  stop: {
    user: string;
    after: number;
  };
  end: {};
}

class EventDischarger<E> {
  emit<Ev extends keyof E>(eventName: Ev, payload: /* ここを埋める */) {
    // 省略
  }
}

// 使用例
const ed = new EventDischarger<EventPayloads>();
ed.emit("start", {
  user: "user1"
});
ed.emit("stop", {
  user: "user1",
  after: 3
});
ed.emit("end", {});

// エラー例
ed.emit<"start" | "stop">("stop", {
  user: "user1"
});
```

### 4-4. 一部だけPartial

標準ライブラリの`Partial`は、オブジェクトの全てのプロパティを省略可能にするものでした。いま、全てではなく一部のプロパティのみ省略可能にしたいです。このような機能を持つ`PartiallyPartial<T, K>`を定義してください。

```ts
// 使用例

// 元のデータ
interface Data {
  foo: number;
  bar: string;
  baz: string;
}
/*
 * T1は { foo?: number; bar?: string; baz: string } 型
 */
type T1 = PartiallyPartial<Data, "foo" | "bar">;
```

### 4-5. 最低一つは必要なオプションオブジェクト

以下のコードで定義される関数`test`は、`foo`, `bar`及び`baz`プロパティを持つオプションオブジェクトを受け取る関数です。これらのプロパティはどれも省略可能としたいですが、全部省略する（すなわち、`{}`を渡される）のだけは型エラーとしたいです。

また、コードを簡単にするために、全てのプロパティが出揃ったオプションオブジェクトの型を下のコードの`Options`で定義し、「各プロパティは省略可能だがいずれか1つは必要なオプションオブジェクトの型」を`AtLeastOne<Options>`で表したいです。このような`AtLeastOne<T>`を定義してください。

```ts
// 使用例
interface Options {
  foo: number;
  bar: string;
  baz: boolean;
}
function test(options: AtLeastOne<Options>) {
  const { foo, bar, baz } = options;
  // 省略
}
test({
  foo: 123,
  bar: "bar"
});
test({
  baz: true
});

// エラー例
test({});
```

### 4-6. ページを描画する関数
以下のコードで定義される`Page`型は、とあるサービスの1ページを表すオブジェクトの型です。このオブジェクトは`page`プロパティを持ち、それで描画すべきページの種類を表しています。また、ページの種類に応じて描画に必要な情報が付加されています。`renderPage`関数は`Page`オブジェクトを受け取って適切にページを描画する関数です（ここでは描画結果は文字列としています）。

また、実際にページを描画する関数はページごとに別々の関数に分かれており、`pageGenerators`オブジェクトにまとまっています。このオブジェクトは`Page`オブジェクトの`page`プロパティに対応する名前のメソッドを持っており、それらのメソッドがそれぞれのページを描画します。メソッドはそのページに対応するオブジェクトを受け取って描画に利用します。

以下のコードに適切な型が付くように`PageGenerators`型を定義してください。

```ts
type Page =
  | {
      page: "top";
    }
  | {
      page: "mypage";
      userName: string;
    }
  | {
      page: "ranking";
      articles: string[];
    };

type PageGenerators = /* ここを埋める */;

const pageGenerators: PageGenerators = {
  top: () => "<p>top page</p>",
  mypage: ({ userName }) => `<p>Hello, ${userName}!</p>`,
  ranking: ({ articles }) =>
    `<h1>ranking</h1>
         <ul>
        ${articles.map(name => `<li>${name}</li>`).join("")}</ul>`
};
const renderPage = (page: Page) => pageGenerators[page.page](page as any);
```

### 4-7. 条件を満たすキーだけを抜き出す

与えられたオブジェクトのプロパティ名のうち、プロパティの値が指定された型であるもののみを抜き出したユニオン型を得る型`KeysOfType<Obj, Val>`を実装してください。

```ts
type KeysOfType<Obj, Val> = /* ここを埋める */;

// 使用例
type Data = {
  foo: string;
  bar: number;
  baz: boolean;

  hoge?: string;
  fuga: string;
  piyo?: number;
};

// "foo" | "fuga"
// ※ "hoge" は string | undefiendなので含まない
type StringKeys = KeysOfType<Data, string>;

function useNumber<Obj>(obj: Obj, key: KeysOfType<Obj, number>) {
  // ヒント: ここはanyを使わざるを得ない
  const num: number = (obj as any)[key];
  return num * 10;
}

declare const data: Data;

// これはOK
useNumber(data, "bar");
// これは型エラー
useNumber(data, "baz");
```

### 4-8. オプショナルなキーだけ抜き出す

与えられたオブジェクトのプロパティ名のうち、オプショナルなプロパティの名前だけを抜き出したユニオン型を得る型`OptionalKeys<Obj>`を実装してください。

```ts
type OptionalKeys<Obj> = /* ここを埋める */

// 使用例
type Data = {
  foo: string;
  bar?: number;
  baz?: boolean;

  hoge: undefined;
  piyo?: undefined;
};

// "bar" | "baz" | "piyo"
type T = OptionalKeys<Data>;
```


## 更新履歴
あとから問題を増やすかもしれないので、更新履歴を用意しておきます。

- 2019-02-23 **20問** (1×4, 2×5, 3×5, 4×6）
- 2020-05-22 **22問** (4-7と4-8を追加)

## まとめ
TypeScriptの型の理解度をはかる問題を作ってみました。全問自力で正解できればTypeScript中級レベルくらいはあると思います。

一応、問題のネタバレになるようなコメントは[解答・解説編](https://qiita.com/uhyo/items/0e7821ce494024c98da5)のほうにお願いします。

## 関連記事
- [TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)
- [TypeScriptの型初級](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f)
- [TypeScriptの型演習（解答・解説編）](https://qiita.com/uhyo/items/0e7821ce494024c98da5)