Closed57

『TypeScriptの型演習』を解く

wsigma21wsigma21

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

  • 正しく型アノテーションをつける
function isPositive(num) {
    return num >= 0;
}

// 使用例
isPositive(3);

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

解答

function isPositive(num: number): boolean {
    return num >= 0;
}
  • 引数の型と返り値の型を追加した
wsigma21wsigma21

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,
};
wsigma21wsigma21

模範解答

  • interface, typeのどちらでも良い
  • interfaceの場合はUserの後に=がないので注意
interface User {
    name: string;
    age: number;
    private: boolean;
}
wsigma21wsigma21

1-3. 関数の型

  • 適切な型IsPositiveFuncを定義する
const isPositive: IsPositiveFunc = num => num >= 0;

// 使用例
isPositive(5);

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

模範解答

  • 引数名は何でもよいが、1つなのでargsよりargが良い
type IsPositiveFunc = (arg: number) => boolean;
wsigma21wsigma21

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"]);
wsigma21wsigma21

解答

function sumOfPos(arr: number[]) :number {
  return arr.filter(num => num >= 0).reduce((acc, num) => acc + num, 0);
}
wsigma21wsigma21

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);

wsigma21wsigma21

検討

ジェネリクスがない場合はこうなる。

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;
}

参考

https://typescriptbook.jp/reference/generics

wsigma21wsigma21

検討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が配列だと伝わってないっぽい

wsigma21wsigma21

解答

渡す型は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);
wsigma21wsigma21

模範解答

  • 戻り値の型をつけ忘れてた。。が、型推論してくれるので省略可とのこと
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;
}
wsigma21wsigma21

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");
wsigma21wsigma21

解答

リテラル型の問題

type Speed = 'slow' | 'medium' | 'fast';
wsigma21wsigma21

模範解答

  • 上記解答と同じ

解説

  • |はユニオン型
  • 本来getSpeed関数に"slow"か"medium"か"fast"以外の引数を渡すとundefinedが返るが、引数にspeed: Speedを設定することで、その3つ以外が渡ったときにエラーが出るようにしている
wsigma21wsigma21

2-3. 省略可能なプロパティ

  • 以下のようなインタフェースを持つ関数addEventListenerdeclareを用いて宣言する
    • 引数は3つ
      • 順に文字列、関数、真偽値またはオブジェクト(省略可能)
        • オブジェクトに存在可能なプロパティはcapture, once, passiveの真偽値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;
wsigma21wsigma21

模範解答

  • capture, once, passiveをオプションにするのを忘れてた、、
  • 第二引数の関数の戻り値の型をvoidにする
  • interfaceにするのはオブジェクト部分だけなのか、、
    • 問題文を読むとどっちにもとれるような気がするけど、自明??
interface AddEventListenerOptionsObject {
  capture?: boolean;
  once?: boolean;
  passive?: boolean;
}
declare function addEventListener(
  type: string,
  handler: () => void,
  options?: boolean | AddEventListenerOptionsObject
): void;
wsigma21wsigma21

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"
});
wsigma21wsigma21

解答

 function giveId<T>(obj: T): T & {id: string}{
  const id = "本当はランダムがいいけどここではただの文字列";
  return {
    ...obj,
    id
  };
}
wsigma21wsigma21

模範解答

  • 上記解答と同じ

解説

  • 返り値の型アノテーションがなくても推論してくれるらしい。
wsigma21wsigma21

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');
wsigma21wsigma21

解答

以下だとsetNumState(3);には対応できるが、setNumState(state => state + 10);のような関数渡しに対応できない。
ユニオン型にすれば良さそうだが、、書き方がわからない。

declare function useState<T>(arg: T): [T, (arg: T) => void];
wsigma21wsigma21

模範解答

  • (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];
wsigma21wsigma21

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");
wsigma21wsigma21

解答

  • 引用するのが嫌なくらいエラーが出てる。。
  • 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;
}
wsigma21wsigma21

模範解答

  • 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;
}
wsigma21wsigma21

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;
  };
}>;
wsigma21wsigma21

解答

  • 元のオブジェクトを受け取って、オプションプロパティにして返したい
  • やりたいことは以下のような感じなんだけど、、上手く書けない
function MyPartial<T> (obj: T) {
    const returnObj = {}
    for (const key in obj) {
        returnObj.push({key? : obj[key]})
    }
}
wsigma21wsigma21

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
});

wsigma21wsigma21

解答

  • 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]) {
    // 省略
  }
}
wsigma21wsigma21

模範解答

  • inじゃなくてextendsを使う
  • <Ev extends keyof E>として、Eに定義されていないイベント名を拒否する
class EventDischarger<E> {
  emit<Ev extends keyof E>(eventName: Ev, payload: E[Ev]) {
    // 省略
  }
}
wsigma21wsigma21

inextendsの違いは?

  • T in keyof E

    • Tinkeyof Eに分けて考える

      • keyof EはEのプロパティ名をユニオンで返している
      • inkeyof Eの中にTが含まれるよう制限
      // 例
      type SystemSupportLanguage = "en" | "fr" | "it" | "es";
      type Butterfly = {
        [key in SystemSupportLanguage]: string;
      };
      
  • T extends keyof E

    • Textendskeyof Eに分けて考える
      • extendsを使うことでジェネリクスの型を限定している
        • Tkeyof Eの部分型でなければならない
wsigma21wsigma21

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,
});
wsigma21wsigma21

解答

  • 素直に考えて以下のように書いたけどエラーになる
    • 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;
  }
};
wsigma21wsigma21

答え

  • 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;
  }
};
wsigma21wsigma21

3-5. undefinedな引数

  • 以下の動作をするように型Func<A, R>を定義しなおす
    • A型の引数を一つ受け取り、R型の値を返す
    • f2のように、Aundefined型なら引数無しで呼べる
      • 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();
wsigma21wsigma21

解答

  • 何もわからない
  • 一応書いてみたのが以下
type Func<A, R> = (arg: A | undefined) => R | undefined;
wsigma21wsigma21

答え

  • undefined extends AundefinedAの部分型かどうかを判定
    • 例えばAがundefined, number | undefinedの場合に合致する
      • このときに引数を省略可能にするのでarg?とする
type Func<A, R> = undefined extends A ? (arg?: A) => R : (arg: A) => R;
wsigma21wsigma21

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);
wsigma21wsigma21

解答

  • unknown云々の前に、「与えられたオブジェクトのfooプロパティを返す関数」の定義ができてない、、
function getFoo<T>(obj: T): typeof T.foo {
  return obj.foo;
}
wsigma21wsigma21

答え

  • <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;
}
wsigma21wsigma21

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 = '';
wsigma21wsigma21

解答

  • 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コンパイラーは指摘しません。

https://typescriptbook.jp/reference/type-reuse/utility-types/omit

wsigma21wsigma21

答え

  • 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かららしい。

https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys

数年前の記事なので返信いただけない可能性も高いが、いちおう、元記事にコメントでOmitでの解答で良いか質問してみた。

このスクラップは3ヶ月前にクローズされました