『TypeScriptの型演習』を解く
Qiitaの記事『TypeScriptの型演習』を解き、調べたことや疑問点をメモしていきます。
自分で解答を作成してみて、TypeScript Playgroundで実行して確かめます。
1-1. 関数に型をつけよう
- 正しく型アノテーションをつける
function isPositive(num) {
return num >= 0;
}
// 使用例
isPositive(3);
// エラー例
isPositive('123');
const numVar: number = isPositive(-5);
解答
function isPositive(num: number): boolean {
return num >= 0;
}
- 引数の型と返り値の型を追加した
1-2. オブジェクトの型
- ユーザーデータのオブジェクトの型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,
};
解答
type User = {
name: string;
age: number;
private: boolean;
}
模範解答
- interface, typeのどちらでも良い
- interfaceの場合はUserの後に
=
がないので注意
interface User {
name: string;
age: number;
private: boolean;
}
interfaceと型エイリアスの違い
どっちを使うべき?という点に関しては以下がわかりやすかった。
基本的にはinterfaceでできることはtype(型エイリアス)でもできそう。
interfaceは拡張性があるが、それをメリットと捉えるかデメリットと捉えるか。
1-3. 関数の型
- 適切な型IsPositiveFuncを定義する
const isPositive: IsPositiveFunc = num => num >= 0;
// 使用例
isPositive(5);
// エラー例
isPositive('foo');
const res: number = isPositive(123);
1-4. 配列の型
- 適当な型アノテーションをつける
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関数は様々な型の配列を受け取れる点に注意
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);
検討
ジェネリクスがない場合はこうなる。
function myFilter(arr: number[], predicate: (arg: number) => boolean) {
const result = [];
for (const elm of arr) {
if (predicate(elm)) {
result.push(elm);
}
}
return result;
}
function myFilter2(arr: string[], predicate: (arg: string) => boolean) {
const result = [];
for (const elm of arr) {
if (predicate(elm)) {
result.push(elm);
}
}
return result;
}
参考
検討2
上記をジェネリクス使って置き換えてみた
function myFilter<T, U>(arr: T, predicate: (arg: U) => boolean) {
const result = [];
for (const elm of arr) {
if (predicate(elm)) {
result.push(elm);
}
}
return result;
}
// 使用例
const res = myFilter<number[], number>([1, 2, 3, 4, 5], num => num % 2 === 0);
const res2 = myFilter<string[], string>(['foo', 'hoge', 'bar'], str => str.length >= 4);
for (const elm of arr)
のarr
でエラーになる。
Type 'T' must have a '[Symbol.iterator]()' method that returns an iterator.
Tが配列だと伝わってないっぽい
解答
渡す型は1つにして、arr
の方はT[]
にして明示的に配列であることを伝える。
これでエラーも出なくなった。
function myFilter<T>(arr: T[], predicate: (arg: T) => boolean) {
const result = [];
for (const elm of arr) {
if (predicate(elm)) {
result.push(elm);
}
}
return result;
}
// 使用例
const res = myFilter<number>([1, 2, 3, 4, 5], num => num % 2 === 0);
const res2 = myFilter<string>(['foo', 'hoge', 'bar'], str => str.length >= 4);
模範解答
- 戻り値の型をつけ忘れてた。。が、型推論してくれるので省略可とのこと
function myFilter<T>(arr: T[], predicate: (elm: T) => boolean): T[] {
const result = [];
for (const elm of arr) {
if (predicate(elm)) {
result.push(elm);
}
}
return result;
}
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. 省略可能なプロパティ
- 以下のようなインタフェースを持つ関数
addEventListener
をdeclare
を用いて宣言する- 引数は3つ
- 順に文字列、関数、真偽値またはオブジェクト(省略可能)
- オブジェクトに存在可能なプロパティはcapture, once, passiveの真偽値3つ(省略可能)
- 順に文字列、関数、真偽値またはオブジェクト(省略可能)
- 引数は3つ
// 使用例
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
は関数や変数の型を中身無しに宣言できる
declare function foo(arg: number): number;
解答
- interfaceの命名に悩んだ
interface addEventListenerInterface {
arg1: string;
arg2: () => {};
arg3?: boolean | {capture: boolean, once: boolean, paddive: boolean};
}
declare function addEventListener(args: addEventListenerInterface): void;
参考
模範解答
- capture, once, passiveをオプションにするのを忘れてた、、
- 第二引数の関数の戻り値の型を
void
にする - interfaceにするのはオブジェクト部分だけなのか、、
- 問題文を読むとどっちにもとれるような気がするけど、自明??
interface AddEventListenerOptionsObject {
capture?: boolean;
once?: boolean;
passive?: boolean;
}
declare function addEventListener(
type: string,
handler: () => void,
options?: boolean | AddEventListenerOptionsObject
): void;
2-4. プロパティを1つ増やす関数
- giveId関数に適切な型をつける
- objが既にidプロパティを持っている場合は考慮しない(4-2で行う)
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
- 以下のようなuseState関数をdeclareで宣言する
// 使用例
// 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');
解答
以下だとsetNumState(3);
には対応できるが、setNumState(state => state + 10);
のような関数渡しに対応できない。
ユニオン型にすれば良さそうだが、、書き方がわからない。
declare function useState<T>(arg: T): [T, (arg: T) => void];
模範解答
-
(arg: T)
の部分をユニオン型にすればよい。 -
UseStateUpdateArgument<T>
のようにTを渡すことで型エイリアスでも同じジェネリクスを使用できる - 返り値を
[T, (updator: UseStateUpdateArgument<T>) => void]
のようにタプル型にする
type UseStateUpdateArgument<T> = T | ((oldValue: T) => T);
declare function useState<T>(
initialValue: T
): [T, (updator: UseStateUpdateArgument<T>) => void];
3-1. 配列から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");
解答
- 引用するのが嫌なくらいエラーが出てる。。
- Keyに使用するのがdataTypeのキーじゃないとダメ、というのを上手く表せなかった、、
type dataType = {
id: number;
name: string;
}
function mapFromArray(arr: dataType[], key<T>: Pick<dataType, T>) {
const result = new Map<number, dataType>();
for (const obj of arr) {
result.set(obj[key], obj);
}
return result;
}
模範解答
- mapFromArrayは2つの型引数
<T, K extends keyof T>
を持つ -
K extends keyof T
は、TのプロパティにKが含まれることを表す- 型引数の制約として、KeyをTのプロパティに限定する
- 返り値の
Map<T[K], T>
は2つの型引数を取る-
T[K]
はキーの型、T
は値の型
-
function mapFromArray<T, K extends keyof T>(arr: T[], key: K): Map<T[K], T> {
const result = new Map();
for (const obj of arr) {
result.set(obj[key], obj);
}
return result;
}
3-2. Partial
- 標準ライブラリに定義されている
Partial
型をMyPartialとして実装する
// 使用例
/*
* T1は { foo?: number; bar?: string; } となる
*/
type T1 = MyPartial<{
foo: number;
bar: string;
}>;
/*
* T2は { hoge?: { piyo: number; } } となる
*/
type T2 = MyPartial<{
hoge: {
piyo: number;
};
}>;
解答
- 元のオブジェクトを受け取って、オプションプロパティにして返したい
- やりたいことは以下のような感じなんだけど、、上手く書けない
function MyPartial<T> (obj: T) {
const returnObj = {}
for (const key in obj) {
returnObj.push({key? : obj[key]})
}
}
模範解答
- Mapped typesを使用している
- インデックス型の設定時に、使用できる型をユニオン型で指定する書き方
-
[K in keyof T]
でオブジェクトTからプロパティKを取り出し、オプションプロパティにする
type MyPartial<T> = {[K in keyof T]? : T[K]};
参考
インデックス型とは
{[K: string]: number;}
みたいに、オブジェクトのフィールド名を設定しない書き方
3-3. イベント
- emitメソッドが間違ったイベント名やデータに対しては型エラーを出すよう、emitメソッドに適切な型をつける
- 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
});
解答
- EventPayloads (E)からkeyだけ取り出してTとして、eventNameはT、payloadはE[T]としたいから以下のように書いたけど、エラー出てる
-
in keyof E
のところで',' expected.
-
E[T]
のところでType 'T' cannot be used to index type 'E'.
-
class EventDischarger<E> {
emit<T in keyof E>(eventName: T, payload: E[T]) {
// 省略
}
}
模範解答
-
in
じゃなくてextends
を使う -
<Ev extends keyof E>
として、Eに定義されていないイベント名を拒否する
class EventDischarger<E> {
emit<Ev extends keyof E>(eventName: Ev, payload: E[Ev]) {
// 省略
}
}
in
とextends
の違いは?
-
T in keyof E
-
T
、in
、keyof E
に分けて考える-
keyof E
はEのプロパティ名をユニオンで返している -
in
でkeyof E
の中にTが含まれるよう制限
// 例 type SystemSupportLanguage = "en" | "fr" | "it" | "es"; type Butterfly = { [key in SystemSupportLanguage]: string; };
-
-
-
T extends keyof E
-
T
、extends
、keyof E
に分けて考える-
extends
を使うことでジェネリクスの型を限定している-
T
はkeyof E
の部分型でなければならない
-
-
-
3-4. reducer
-
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,
});
解答
- 素直に考えて以下のように書いたけどエラーになる
- amount, valueがundefinedの可能性があり、演算できない、戻り値をnumberに限定できないといった問題がある
interface actionInterface {
type: string;
amount?: number;
value?: number;
}
const reducer = (state: number, action: actionInterface): number => {
switch (action.type) {
case "increment":
return state + action.amount;
case "decrement":
return state - action.amount;
case "reset":
return action.value;
}
};
答え
- action.typeごとにユニオン型で定義する
-
type: string
じゃなくてtype: "increment"
のように指定するのがポイント
-
type ActionType =
{
type: "increment";
amount: number;
}
|
{
type: "decrement";
amount: number;
}
|
{
type: "reset";
value: number;
};
const reducer = (state: number, action: ActionType) => {
switch (action.type) {
case "increment":
return state + action.amount;
case "decrement":
return state - action.amount;
case "reset":
return action.value;
}
};
3-5. undefinedな引数
- 以下の動作をするように型
Func<A, R>
を定義しなおす-
A
型の引数を一つ受け取り、R
型の値を返す -
f2
のように、A
がundefined
型なら引数無しで呼べる-
v3
のように、明示的にundefined
を渡して呼びだせる
-
-
v4
のように、引数がundefined
以外の時は引数の省略は許さない
-
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();
解答
- 何もわからない
- 一応書いてみたのが以下
type Func<A, R> = (arg: A | undefined) => R | undefined;
答え
-
undefined extends A
でundefined
がA
の部分型かどうかを判定- 例えばAが
undefined
,number | undefined
の場合に合致する- このときに引数を省略可能にするので
arg?
とする
- このときに引数を省略可能にするので
- 例えばAが
type Func<A, R> = undefined extends A ? (arg?: A) => R : (arg: A) => R;
undefined
の可能性がある
オプショナルなプロパティにアクセスすると?修飾子を付けられたプロパティを取得する場合は自動的にundefined型とのunion型になります
参考
4-1. 無い場合はunknown
-
getFoo
関数に適切な型をつける- 引数に渡されたオブジェクトの型に応じて返り値の型を適切に変化させる
-
foo
プロパティを持たないオブジェクトを渡された場合は、返り値の型をunknown
とする
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);
解答
-
unknown
云々の前に、「与えられたオブジェクトのfoo
プロパティを返す関数」の定義ができてない、、
function getFoo<T>(obj: T): typeof T.foo {
return obj.foo;
}
答え
-
<T>(obj: T)
ではなく<T extends object>(obj: T)
とすることで、オブジェクト以外が引数として渡されないようにしている - 返り値で
T extends { foo: infer E } ? E : unknown
のようにconditional typeを使用する-
T extends { foo: infer E }
で、Tがfoo
プロパティを持つ型かどうかで場合分け- 持つ場合は
foo
プロパティの型をE
として取得する
- 持つ場合は
-
- 返り値が
obj.foo
だとエラーProperty 'foo' does not exist on type 'T'.
になる-
(obj as any)
とすることでエラーを抑制
-
function getFoo<T extends object>(
obj: T
): T extends { foo: infer E } ? E : unknown {
return (obj as any).foo;
}
inferとは何か
Conditional type
T extends U ? X : Y
の条件(Uのとこ)にinfer S
と書くと、Sに補足された型を X の部分で再利用可能になります。
as anyとは何か
-
obj as any
でobjをany型にキャスト型アサーションしている
キャストとは、実行時にある値の型を別の型に変換することです。
型アサーションは、実行時に影響しません。値の型変換はしないのです。あくまでコンパイル時にコンパイラーに型を伝えるだけです。
4-2. プロパティを上書きする関数
- 2-4では
obj
が既に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 = '';
解答
- TにあるidをOmitで取り除く
- Tにidがない場合でもエラーにはならない
function giveId<T>(obj: T): Omit<T, "id"> & { id: string } {
const id = "本当はランダムがいいけどここではただの文字列";
return {
...obj,
id
};
}
参考
Omit<T, Keys>のKeysにTには無いプロパティキーを指定しても、TypeScriptコンパイラーは指摘しません。
答え
-
Exclude<keyof T, "id">
でTのプロパティからidを取り除く -
Pick<T, Exclude<keyof T, "id">>
で、Tからid以外のプロパティを取り出す
function giveId<T>(obj: T): Pick<T, Exclude<keyof T, "id">> & { id: string } {
const id = "本当はランダムがいいけどここではただの文字列";
return {
...obj,
id
};
}
答えの検討
わざわざこんなことしなくてもOmit
を使えばいいんじゃないか?と思ったら、この演習問題は執筆当時の最新であるTypeScript 3.3.3333を使っており、Omit
がリリースされたのはTypeScript3.5かららしい。
数年前の記事なので返信いただけない可能性も高いが、いちおう、元記事にコメントでOmit
での解答で良いか質問してみた。