この記事は、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つで、全て真偽値であり、省略可能です。
このようなインターフェースを持つ関数addEventListener
をdeclare
を用いて宣言してください。簡単のために、第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 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
の使い方は以下の使用例も参考にしてください。
このようなuseState
をdeclare
で宣言してください。ただし、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
のようにA
がundefined
型のときは引数無しで呼べるようにしたいです。一応、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の標準ライブラリで定義されている型(Record
、Partial
など)を使用しても構いません。また、これまでと同様に適宜型引数を用いたりas
を使用したりしても構いません。
4-1. 無い場合はunknown
以下のコードで定義されるgetFoo
は、与えられたオブジェクトのfoo
プロパティを返す関数です。この関数に適切な型を付けてください。
ただし、foo
プロパティがstring
型を持つオブジェクトを渡されたらgetFoo
の返り値の型がstring
になる、というように、引数に応じてgetFoo
の型が適切に変化するようにしてください。また、foo
プロパティを持たないオブジェクトを渡された場合は、エラーではなく返り値の型がunknown
となるようにしてください。さらに、123
やnull
など、オブジェクトでない値を渡すのは型エラーとなるようにしてください。
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 = '';
がエラーになりますが、今回はこれがエラーにならないようにする必要があります。
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中級レベルくらいはあると思います。
一応、問題のネタバレになるようなコメントは解答・解説編のほうにお願いします。