Help us understand the problem. What is going on with this article?

TypeScriptの型演習

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

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

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

問題の解答・解説はTypeScriptの型演習(解答・解説編)を参照してください。

難易度:★☆☆☆ 基礎の基礎レベル

基本的な型が分かっていれば解ける問題たちです。

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

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

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を定義してください。

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を定義してください。

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

// 使用例
isPositive(5);

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

1-4. 配列の型

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

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に型引数を追加しても構いません。

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を定義してください。

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は、2つまたは3つの引数を受け取る関数で、返り値はありません。1つ目の引数は文字列、2つ目の引数は関数です。そして3つ目の引数は省略可能であり、真偽値またはオブジェクトを渡すことができます。オブジェクトに存在可能なプロパティはcapture, once, passiveの3つで、全て真偽値であり、省略可能です。

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

// 使用例
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に特有の構文であり、以下のように関数や変数の型を中身なしに宣言できる構文です。

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

2-4. プロパティを1つ増やす関数

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

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の使い方は以下の使用例も参考にしてください。

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

// 使用例
// 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の型入門の内容をとりあえず知っていれば解ける問題たちです。

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

3-1. 配列からMapを作る

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

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という名前でこれを実装してください。

// 使用例
/*
 * 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に定義されたイベントを適切に受け付ける必要があります。

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に適切な型をつけてください。

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のようにAundefined型のときは引数無しで呼べるようにしたいです。一応、v3のように明示的にundefinedを渡して呼び出すのもOKとしたいです。一方、v4のように、引数がundefined以外のときは引数の省略は許しません。

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

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の型初級の内容を含む問題たちです。解答にはTypeScriptの標準ライブラリで定義されている型(RecordPartialなど)を使用しても構いません。また、これまでと同様に適宜型引数を用いたりasを使用したりしても構いません。

4-1. 無い場合はunknown

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

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

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

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の型を完成させてください。

ヒント(白い文字で書かれています): Evがユニオン型のときはpayloadの型をneverにしましょう。

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>を定義してください。

// 使用例

// 元のデータ
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>を定義してください。

// 使用例
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型を定義してください。

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>を実装してください。

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>を実装してください。

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中級レベルくらいはあると思います。

一応、問題のネタバレになるようなコメントは解答・解説編のほうにお願いします。

関連記事

uhyo
Metcha yowai software engineer
https://uhy.ooo/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした