Open22

TypeScriptについての覚え書き

aibizaibiz

オブジェクト型の書き方

const obj: {
  foo: number;
  bar: string;
} = {
  foo: 123,
  bar: "Hello, world!"
};
aibizaibiz

型エイリアス(type文で型に別名をつける)

type FooBarObj = {
  foo: number;
  bar: string;
};

const obj: FooBarObj = {
  foo: 123,
  bar: "Hello, world!"
};
aibizaibiz

interface宣言でオブジェクト型を宣言する

interface FooBarObj {
  foo: number;
  bar: string;
}

const obj: FooBarObj = {
  foo: 0,
  bar: "string"
};
aibizaibiz

ジェネリック型(型引数を持つ型の宣言)

型引数は、型を定義するときにパラメータをもたせることができる。
型引数は、type文(またはinterface宣言)で作成するときに宣言する。
型引数を宣言すると、その型引数は、その宣言の中(type文の=の右側)でだけ有効な型名として扱われる。

例1)

型引数Tを持つUser型を宣言

type User<T> = {
  name: string;
  child: T;
};

例2)

型引数は複数あっても良い。

type Family<Parent, Child> = {
  mother: Parent;
  father: Parent;
  child: Child;
};

ジェネリック型(型引数を持つ型の宣言)を使用する。
Family<number, string>

Family型が持つ2つの型引数Parent, Childにそれぞれ、number, string型を当てはめた型

const obj: Family<number, string> = {
  mother: 0,
  father: 100,
  child: "1000"
};

部分型関係による型引数の制約

type HasName = {
  name: string;
};
type Family<Parent extends HasName, Child extends HasName> = {
  mother: Parent;
  father: Parent;
  child: Child;
};

オプショナルな型引数

type Animal = {
  name: string;
}
type Family<Parent = Animal, Child = Animal> = {
  mother: Parent;
  father: Parent;
  child: Child;
}

// 通常どおりの使い方
type S = Family<string, string>;
// TはFamily<Animal, Animal>と同じ
type T = Family;
// UはFamily<string, Animal>と同じ
type U = Family<string>;
aibizaibiz

関数宣言(function declaration)で関数を作る

//function 関数名(引数: 引数の型, ... ): 戻り値の型 { 処理内容 }
function range(min: number, max: number): number[] {
  const result = [];
  for (let i = min; i <= max; i++) {
    result.push(i);
  }
  return result;
}

console.log(range(5, 10)); // [5, 6, 7, 8, 9, 10] と表示される
aibizaibiz

関数式(function expression)で関数を作る

type Human = {
  height: number;
  weight: number;
};

// function(引数: 引数の型, ... ): 戻り値の型 { 処理内容 }
const calcBMI = function(human: Human): number {
  return human.weight / human.height ** 2;
};

const uhyo: Human = { height: 1.84, weight: 72 };
// 21.266540642722116 と表示される
console.log(calcBMI(uhyo));
type Human = {
  height: number;
  weight: number;
};

//Human型を{ height, weight }に分割代入もできる
const calcBMI = function({ height, weight }: Human): number {
  return weight / height ** 2;
};

const uhyo: Human = { height: 1.84, weight: 72 };
// 21.266540642722116 と表示される
console.log(calcBMI(uhyo));
aibizaibiz

アロー関数式(arrow function expression)で関数を作る

type Human = {
  height: number;
  weight: number;
};

//アロー関数
const calcBMI = ({
  height, weight
}: Human): number => {
  return weight / height ** 2;
};

const uhyo: Human = { height: 1.84, weight: 72 };
// 21.266540642722116 と表示される
console.log(calcBMI(uhyo));

アロー関数の省略形

式が1文の場合は returnと{}を省略できる

//アロー関数の省略形
const calcBMI = ({
  height, weight
}: Human): number => weight / height ** 2;

戻り値にオブジェクトリテラルを使う場合の注意

オブジェクトリテラルを()で囲む。
囲まないと、アロー関数の中身を囲み{}と見なされてしまう。

type Human = {
  height: number;
  weight: number;
};
type ReturnObj = {
  bmi: number
}
// 正しい書き方
const calcBMIObject = ({
  height, weight
}: Human): ReturnObj => ({
  bmi: weight / height ** 2
});

// これはコンパイルエラーが発生
// エラー: A function whose declared type is neither 'void' nor 'any' must return a value.
const calcBMIObject2 = ({
  height, weight
}: Human): ReturnObj => {
  bmi: weight / height ** 2;
};
aibizaibiz

メソッド記法で関数を作る

この記法はオブジェクトリテラルの中で使用することができるプロパティを定義する記法の一種です。

const obj = {
  // メソッド記法
  double(num: number): number {
    return num * 2;
  },
  // 通常の記法 + アロー関数
  double2: (num: number): number => num * 2,
};

console.log(obj.double(100));  // 200 と表示される
console.log(obj.double2(-50)); // -100 と表示される
aibizaibiz

関数型の記法

関数xRepeatの型は?
(num: number) => string 型

関数型は(引数リスト) => 戻り値の型 という形をとる

const xRepeat = (num: number): string => "x".repeat(num);

引数なし、返り値なしの関数の関数型

() => void

0個以上任意の数の数値を受け取り、数値を返す関数の型

(...args: number[]) => number

type文でF型として別名を定義して、関数の型注釈に使用できる。

type F = (repeatNum: number) => string;

const xRepeat: F = (num: number): string => "x".repeat(num);

関数の返り値の型注釈は省略できる

const xRepeat = (num: number) => "x".repeat(num);
aibizaibiz

async関数/await式

async関数

返り値は必ずPromiseになる。
例)get3()関数は「3を結果とするPromise」を返す関数である。

async function get3(): Promise<number> {
  return 3;
}
const p = get3();
p.then(num => {
  console.log(`num is ${num}`);
});

await式

await式はasync関数の中で使える構文。
与えられたPromiseの結果が出るまで待つ

const sleep = (duration: number) => {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, duration);
  });
};

async function get3() {
  await sleep(1000);
  return 3;
}

const p = get3();
p.then(num => {
  console.log(`num is ${num}`);
});

別の例

const sleep = (duration: number) => {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, duration);
  });
};

async function get3() {
  console.log("get3が呼び出されました");
  await sleep(1000);
  console.log("awaitの次に進みました");
  return 3;
}

console.log("get3を呼び出します");
const p = get3();
p.then((num) => {
  console.log(`num is ${num}`);
});
console.log("get3を呼び出しました");

結果
get3を呼び出します
get3が呼び出されました
get3を呼び出しました
(1秒後)
awaitの次に進みました
num is 3

別の例

const sleep = (duration: number) => {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, duration);
  })
};

async function get3() {
  await sleep(1000);
  return 3;
}

async function main() {
  const num1 = await get3();
  const num2 = await get3();
  const num3 = await get3();
  return num1 + num2 + num3;
}

main().then(result => {
  console.log(`result is ${result}`);
});

async関数のいろいろな宣言方法

async function式

const main = async function() {
  const fooContent = await readFile("foo.txt", "utf8");
  // 2倍にしてbar.txtに書き込む
  await writeFile("bar.txt", fooContent + fooContent);
  console.log("書き込み完了しました");
};

asyncアロー関数式

const main = async () => {
  const fooContent = await readFile("foo.txt", "utf8");
  // 2倍にしてbar.txtに書き込む
  await writeFile("bar.txt", fooContent + fooContent);
  console.log("書き込み完了しました");
};

asyncメソッド記法でのasync関数

const obj = {
  // 普通のメソッド
  normalMethod() {
    // (略)
  },
  // async関数のメソッド
  async asyncMethod() {
    // (略)
  }
};
aibizaibiz

ジェネリック関数のバリエーション

const repeat = function<T>(element: T, length: number): T[] {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
}
const repeat = <T>(element: T, length: number): T[] => {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
}
const utils = {
  repeat<T>(element: T, length: number): T[] {
    const result: T[] = [];
    for (let i = 0; i < length; i++) {
      result.push(element);
    }
    return result;
  }
}
const pair = <Left, Right>(left: Left, right: Right): [Left, Right] => [left, right];
// pは[string, number]型
const p = pair("uhyo", 26);
const repeat = <T extends {
  name: string;
}>(element: T, length: number): T[] => {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
}

type HasNameAndAge = {
  name: string;
  age: number;
}

// これはOK
// 出力結果:
// [{
//   "name": "uhyo",
//   "age": 26
// }, {
//   "name": "uhyo",
//   "age": 26
// }, {
//   "name": "uhyo",
//   "age": 26
// }] 
console.log(repeat<HasNameAndAge>({
  name: "uhyo",
  age: 26,
}, 3));
// これはコンパイルエラー
// エラー: Type 'string' does not satisfy the constraint '{ name: string; }'.
console.log(repeat<string>("a", 5));
aibizaibiz

typeofキーワードで変数の型を得る

型推論の結果を型として抽出・再利用したい場合にtypeofは効果的。
特に、ランタイム(すなわち、型ではない)変数宣言から型推論を通じて型を取り出さるというのは非常に協力な機能。

const obj = {
  foo: 123,
  bar: "hi"
};

type T = typeof obj;

const obj2: T = {
  foo: -50,
  bar: ""
};
aibizaibiz

ネストされたオブジェクトについての注意

https://react.dev/learn/updating-objects-in-state

スプレッド構文を使用したオブジェクトのコピー

...スプレッド構文は「浅い」ことに注意してください。(シャローコピー)
つまり、1 レベルの深さまでしかコピーされません。
https://react.dev/learn/updating-objects-in-state#copying-objects-with-the-spread-syntax

ネストされたオブジェクトの更新

https://react.dev/learn/updating-objects-in-state#updating-a-nested-object

Immer を使用して簡潔な更新ロジックを作成する

https://react.dev/learn/updating-objects-in-state#write-concise-update-logic-with-immer

Immer

https://immerjs.github.io/immer/

aibizaibiz

部分型関係(subtyping relation)

部分型関係は、

  • ある型をほかの型の代わりに使えるか
  • ある型をほかの型とみなせるか
    という直感的な概念に基づくものである。

例1)

type FooBar = {
  foo: string;
  bar: number;
}
type FooBarBaz = {
  foo: string;
  bar: number;
  baz: boolean;
}

const obj: FooBarBaz = {
  foo: "hi",
  bar: 1,
  baz: false
};
const obj2: FooBar = obj;

FooBarBaz型は、FooBar型の部分型である。
つまり、
FooBarBaz型の値は、FooBar型の値でもある。
FooBarBaz型は、FooBar型の上位互換。

  1. FooBar型が持つプロパティはすべてFooBarBaz型にも存在する。
  2. 条件1の各プロパティについて、FooBarBaz型におけるそのプロパティの型はFooBar型におけるそのプロパティの型の部分型(または同じ型)である。

例2)

type Animal = {
  age: number;
};
type Human = {
  age: number;
  name: string;
}

Human型はAnimal型の部分型である。

例3)

type AnimalFamily = {
  familyName: string;
  mother: Animal;
  father: Animal;
  child: Animal;
}
type HumanFamily = {
  familyName: string;
  mother: Human;
  father: Human;
  child: Human;
}

HumanFamily型はAnimalFamily型の部分型である。

aibizaibiz

配列型の記法

T[] で表現

const arr: number[] = [1, 10, 100];

const arr2: string[] = ["123", "-456"];

Array<T> で表現

const arr3: Array<{
  name: string;
}> = [
  { name: "山田さん" },
  { name: "田中さん" },
  { name: "鈴木さん" }
];

異なる型が混ざった配列の型も表現可能

// (string | number | boolean)[]型
const arr4 = [100, "文字列", false];
aibizaibiz

ジェネリック関数(型引数を持つ関数)

関数宣言

function repeat<T>(element: T, length: number): T[] {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
}

// ["a", "a", "a", "a", "a"] が表示される
console.log(repeat<string>("a", 5)); 
// [123, 123, 123] が表示される
console.log(repeat<number>(123, 3)); 

関数式

const repeat = function<T>(element: T, length: number): T[] {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
}

アロー関数式

const repeat = <T>(element: T, length: number): T[] => {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(element);
  }
  return result;
}

メソッド記法

const utils = {
  repeat<T>(element: T, length: number): T[] {
    const result: T[] = [];
    for (let i = 0; i < length; i++) {
      result.push(element);
    }
    return result;
  }
}