#
ドキュメント

Document

自分のための備忘録です。

TypeScript

「プロを目指す人のためのTypeScript入門」の読書メモです。

TypeScript とは

TypeScript は JavaScript静的型付け を追加した言語です。

開発環境

  1. Node.js をインストール
    • TypeScript コンパイラは Node.js を使用
  2. TypeScript をインストール

[!NOTE] コード内で Node.js を使用する場合( require など)は @type/node もインストールします。

TSC(TypeScript Compiler)

  1. 型チェック
  2. トランスパイル(コンパイルとも言える)

トランスパイルでどのバージョンの JavaScript(ECMAScript)[^1] に変換するのかを ts.config の target プロパティで指定します。

[^1]: 例) es5, es2015, es2020 など。

[!NOTE] tsc に個別ファイルを指定した場合は ts.config を無視します( $ npx tsc /path/to/file.ts )。

開発の流れ

  1. TypeScript から JavaScript にトランスパイル( tsc )
  2. 1. でトランスパイルした JavaScript を Node.js で実行( node )

JavaScriptの値と型

  • オブジェクト型
  • 関数型
  • プリミティブ型
    • 文字列型
    • 数値型
    • 真偽値型
    • BigInt型
    • null型: null 型の値は null のみ
    • undefined型: undefined 型の値は undefined のみ[^undefined]
    • シンボル型

JavaScript は存在しないプロパティにアクセスした場合は undefined を返します。

文( sentence )と式( expression )

はセミコロンで区切られて上から順に実行します[^1]。

宣言( declaration )とその他に分けられます。
例) 例えば(正式には) 変数宣言文とは呼ばす 変数宣言 、関数宣言文とは呼ばず 関数宣言 です。

[^1]: 制御構文、関数宣言( function Foo() {} )の巻き上げ、type 文のコンパイル時チェックなどの例外があります。また行末のセミコロンは省略可能です。

  • 式は結果がある
  • 式(及び後述する式文)で文を構成する

式は一般に何らかの計算を表します。そして、その計算の結果が式の結果となるのです。

出典:プロを目指す人のためのTypeScript入門 p27

式文

式文は、式のあとにセミコロンを書く文です。

文と式の違い

文と式の決定的な違いは何でしょうか。それは結果があるかどうかです

出典:プロを目指す人のためのTypeScript入門 p27

サンプル

/*
 * 文の例
 */
// 文1 変数宣言
const hello = 'Hello';
// 文2 関数宣言
function getHello('name': string): string {
    return `Hello ${name}!`;
}
function loggingHello('name': string): void {
    console.log(`Hello ${name}!`);
}

/*
 * 式
 */
// 例1
'Hello'
// 例2
3 + 5 // 式 + 式
// 例3(変数名も式であることに注意)
foo

/*
 * 式文
 */
// 式文1 代入
message = 'Hello World'; // 代入演算子の左辺にある変数名( message )も式になる
// 式文2 戻り値のない関数呼び出し
loggingHello('Hiroshi');

ブロック

ブロックとは、{ }の中に文をいくつでも書けるとう構文で、ブロック自体は1つの文として扱われます。ブロックを文として実行する場合、その中に書かれた文が上から順番に実行されます。

出典:プロを目指す人のためのTypeScript入門 p63

ブロックが文の一種であるということは、if (条件式) 文という構文の文のところにブロックを置くことができるということです。

出典:プロを目指す人のためのTypeScript入門 p63

ブロックの使い道

  • 複数の文を1つの文にまとめる 出典:プロを目指す人のためのTypeScript入門 p63

  • ブロックスコープの導入 出典:プロを目指す人のためのTypeScript入門 p63

リテラル

リテラルとは、何らかの値を生み出すための式のこと

出典:プロを目指す人のためのTypeScript入門 p35

  • 文字リテラル
  • 数値リテラル
  • オブジェクトリテラル
  • ....

== と null

x == null は x が nullundefined のときのみ ture になります。 null は良くて undefined は駄目な場合は x === null を使います 。

=== NaN

x === NaN は x がどのような値でも( NaN であっても)必ず false になります。 NaN を判定したいときは Number.isNaN() を使用します。

?? ( null 合体演算子)

x ?? y は x が null または undefined のときのみ y を返します。

出典:プロを目指す人のためのTypeScript入門 p56

x || y は x が 0 や 空文字や false の場合も y を返します。

オブジェクト

部分型関係( subtypig relation )

簡潔にまとめると部分型は親の代替ができる。

型Sと型Tがオブジェクト型だとして、次の2つの条件が満たされればSがTの部分型になります。

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

出典;プロを目指す人のためのTypeScript入門 p97

type 文 で型に別名を付ける

type 文は オブジェクト型 だけでなく プリミティブな型 の別名(型名)を作成できます。

type文は決して「新たに型を作って利用可能にする」ものではなく、「すでにある型に別名をつける」だけのもの

出典:プロを目指す人のためのTypeScript入門 p89

// オブジェクト型
type MyObject: {
  name: string;
  age: string;
}
// string 型
type MyString = string;

type 文による型の作成はその型名の使用より後でも問題ありません(型チェックはコンパイル時に実行するため)。

型引数( type parameters )

型引数は、型を定義するときにパラメータをもたせることができるというもので、ジェネリクスに少し似ています。
出典:プロを目指す人のためのTypeScript入門 p100

ジェネリクス(generics)とは、型引数を受け取る関数を作る機能のことです。
出典:プロを目指す人のためのTypeScript入門 p168

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

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

適切な数の型引数を指定せずに型を使用した場合はコンパイルエラという結果になります。 出典:プロを目指す人のためのTypeScript入門 p101

// エラー: Generic type 'Family' requires 2 type argument(s).
const obj: Family = {
    mother: 0,
    father: 100,
    child: "1000",
}

interface 宣言

interface 宣言 で作成できる型名はオブジェクト型のみです。 多くのの場合は type 文 で代用可能です。

for-of 文とイテレータ

TypeScriptにはイテレータ(Iterator)という概念が存在します。 ... イテレータは繰り返し処理のための汎用的なインターフェースです。イテレータを通じて繰り返し処理ができる値のことをJavaScript用語でIterableと呼びます。for-of文のofの右の式には、厳密には配列だけでなくそれ以外のIterableな値を与えることができまるのです。 出典:プロを目指す人のためのTypeScript入門 p111)

イテレータのインターフェース

より具体的に言うと、イテレーターは、次の 2 つのプロパティを持つオブジェクトを返す next() メソッドを持つことによってイテレータープロトコルを実装するオブジェクトです。

value 反復シーケンスの次の値

done シーケンスの最後の値が既に消費されている場合に true となります。done と並んで value が存在する場合、それがイテレーターの戻り値となります。

出典:イテレーターとジェネレーター

イテレーターの例(ジェネレーター)

function *createGenerator() {
    const limit = 10;
    for (let i = 0; i < limit; i++ ) {
      yield i;
    }
}

const iterator = createGenerator();

for (const value of iterator) {
    console.log(value);
}

// 以下と同じ挙動

/* while(true) {
  const result = iterator.next();
  if ( result.done ) { 
    break;
  }
  console.log(result.value)
} */

スプレッド構文

const obj1 = {
  name: 'John',
  age: 30,
}

const obj2 = {
  hobby: 'Reading',
  ...obj1,
}

console.log(obj2);
/*
{
  "hobby": "Reading",
  "name": "John",
  "age": 30
}
*/

// typeof で動的に型名を作成
type Obj2nd = typeof obj2; 

typeof

typeof には typeof キーワードと typeof 演算子の 2 種類ります。混同しないように注意が必要です。

  • typeof キーワード: 型を作るキーワード type 文 で使用
  • typeof 演算子[^typeof]

[^typeof]: typeof null が object になることに注意してください(歴史的経緯)。

分割代入( Destructuring )

オブジェクトのプロパティの中身をプロパティと同名の変数に入れたい場合にしかこのパターンを用いることができません。

出典:プロを目指す人のためのTypeScript入門 p114

const rect = {width: 100, height: 200, depth: 300};
const {width, height, depth} = rect;
console.log(width, height, depth);
// 100, 200, 300

https://typescript-jp.gitbook.io/deep-dive/future-javascript/destructuring

分割を使用して構造体の深いデータを取得

出典: https://typescript-jp.gitbook.io/deep-dive/future-javascript/destructuring

const page = {
    title: 'Hello World',
    content: {
        catchCopy: 'HELLO WORLD',
        lead: 'Welcome to my page.',
        body: 'This is content'
    }
}

var {content: {catchCopy}} = page;
console.log(catchCopy);
// HELLO World
console.log(content);
// ERR content is not defined
var { content } = page;
console.log(content);

//
{
  body: "This is content",
  catchCopy: "HELLO WORLD",
  lead: "Welcome to my page."
}

プロパティ名に別の変数名を使う

contentfoo に代入します。

const page = {
    title: 'Hello World',
    content: {
        catchCopy: 'HELLO WORLD',
        lead: 'Welcome to my page.',
        body: 'This is content'
    }
}
const {content: foo} = page;
console.log(foo);

//  {
//  "catchCopy": "HELLO WORLD",
//  "lead": "Welcome to my page.",
//  "body": "This is content"
// } 

スプレッド構文と分割代入を使ったオブジェクトのコピー

スプレッド構文

const obj1 = {
    foo: 'a',
    bar: 'b',
    baz: 'c',
};
const obj2 = {...obj1};
console.log(obj2);
/*
{
  "foo": "a",
  "bar": "b",
  "baz": "c"
}
*/

obj1.foo = 'x';
console.log(obj2.foo); // a

分割構文

const obj1 = {
    foo: 'a',
    bar: 'b',
    baz: 'c',
};
const {...obj2} = obj1;
console.log(obj2);
/*
{
  "foo": "a",
  "bar": "b",
  "baz": "c"
}
*/

obj1.foo = 'x';
console.log(obj2.foo); // a

関数

  • TypeScript は return しなかった関数は undefined とみなす

オプショナルな引数

function foo( a?: string ){
    // 呼び出しで引数が省略されたときの a は undefined
    if (a) {
        //
    } else {
        //
    }
}

関数型の型名

関数型宣言構文: type 型名 = (引数名: 型名) => 戻り値型

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

// 関数型の型を明記
type GetNameFunc = (user: User) => string;

const getName: GetNameFunc = (user: User): string => {
  return user.name;
}

console.log(getName({ name: 'John', age: 30 })); // John

/*
 * コンパイル時の型チェックでエラー
 * // 関数の型チェックエラー文
 * > Type '(name: string) => string' is not assignable to type 'GetNameFunc'.Types of parameters 'name' and 'user' are incompatible.
 *
 * // 呼び出し時の型チェックのエラー文
 * > Type 'User' is not assignable to type 'string'. const getNameFaulty: GetNameFunc = (name: string):string => {
 *   return name;
 * }
 */
console.log(getNameFaulty('jhon')) // 文字型を渡しているので型チェックエラー

ジェネリクス

ジェネリクス(generics)とは型引数を受け取る関数を作る機能のことです。

出典:プロを目指す人のためのTypeScript入門 p168

// 出典:プロを目指す人のためのTypeScript入門 p169
function repeat <T>(elem: T, num: number): T[] {
  const result: T[] = [];
  for (let i = 0; i < num; i++ ) {
    result.push(elem)
  }
  return result;
}

console.log(repeat('a', 5)); // ["a", "a", "a", "a", "a"]
console.log(repeat(123, 3)); // [123, 123, 123]
// 関数の型名をジェネリクスで定義
type Echo<T> = (value: T) => T;

const echoString: Echo<string> = (str: string) => {
   return str;
}
const echoNumber: Echo<number> = (num: number) => {
   return num;
}

console.log(echoString('Hello')); // Hello
console.log(echoNumber(123)); // 123

以下のように簡潔にかけます。

const echo = <T>(value:T): T => {
  return value;
}

console.log(echo('HelHello'));
console.log(echo(123));

コールバック関数

type MyCallback<T> = (value: T[]) => T[];

const caller = <T>(callback: MyCallback<T>, args: T[]): T[] => {
  return callback(args);
}

const result = caller((nums: number[]) => {
  return nums.map(num => num * 2);
}, [1, 2, 3]);

console.log(result); // 2 4 6

関数の部分型関係

親と代替できることが重要です。
より少ない入力(引数)からより多い出力(戻り値)を返します。

出典:プロを目指す人のためのTypeScript入門 p165

引数の型アノテーションを省略可能な場合

から関数式の内部(ここでは引数型)を推論(逆方向の型推論と呼ぶ)できる場合は型アノテーションを省略できる。
逆方向の型推論を contextual typing と呼びます。

type F = (value: string) => string;

// 引数の型アノテーションを省略する場合
const getName: F = (name) => return name;

// 引数の型アノテーションを省略しない場合
const getName: F = (name: string): string => return name;

クラス

クラス宣言の重要な特徴のひとつは、クラスオブジェクトという値を作るものであると同時に、インスタンスの型を宣言するものであるということです。

出典:プロを目指す人のためのTypeScript入門 p201

変数名と型名の名前空間

名前には2種類あることを理解する必要があります。すなわち、変数名と型名です。これらは別々の名前空間に属しています。

出典:プロを目指す人のためのTypeScript入門 p203

instanceof 演算子

class User {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

const user1 = new User('user1', 18);

// user1 は User のインスタンス
console.log(user1 instanceof User) // true

// class User { } で User 型が定義されていることに注意
const user2: User = {
    name: 'user2',
    age: 19
}

// user2 は User 型として扱えるが User のインスタンスではない
console.log(user2 instanceof User) // false

// typeof 演算子は object
console.log(typeof user1, typeof user2); // "object", "object"

クラス宣言とプロトタイプ

クラスについて MDN に以下のように記載されています。

JS のクラスはプロトタイプに基づいて構築されていますが、一部の構文や意味はクラスに固有です。

出典:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes

「一部の構文や意味はクラスに固有です」との記載があるのでクラス が ES 5 のプロトタイプベースの糖衣構文と言い切れるかはわかりません。

class User {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}
/*
 * TypeScript は JavaScipt のスーパセットなので prototype 継承も有効
 */
// new User() でインスタン( user1 )を作成
const user1 = new User('Jhon', 25);
// インスタンス( user1 )はコンストラクタ( User )の prototype へのリンクを持つ
console.log(User.prototype === Object.getPrototypeOf(user1)); // true
console.log(user1.constructor === User); // true

const user2: User = {
  name: 'Jhon',
  age: 25
}
// user2 は( User 型のオブジェクトだが)User のインスタンスではない
// ↑より User.prototype へのリンクは持たない
console.log(User.prototype === Object.getPrototypeOf(user2)); // false
console.log(user2.constructor === User); // false
// user2 は Object のインスタンス(オブジェクトリテラル {} は new Object の頭囲構文)
// ↑より user2 は Object.protype へのリンクを持つ
console.log(Object.prototype === Object.getPrototypeOf(user2)); // true
console.log(user2.constructor === Object); // true

クラスのメソッドは protoype のメソッドになる。

class Product {
    #name: string;
    #price: number;

    constructor(name: string, price: number) {
        this.#name = name;
        this.#price = price;
    }

    getOrder(num: number): string {
        return `${this.#name} is ${this.#price * num}`
    }
}


const product1 = new Product('product1', 100);
const product2 = new Product('product2', 200);

console.log(typeof product1.getOrder, typeof product2.getOrder); // function, function
console.log(product1.getOrder === product2.getOrder); // true
console.log(product1.getOrder === Product.prototype.getOrder); // true
console.log(product2.getOrder === Product.prototype.getOrder); // true

継承

また、コンストラクタもオーバーライドすることができます。ただし、その場合は子クラスのコンストラクタの引数の中にsuper呼び出しを含める必要があります。

プロを目指す人のためのTypeScript入門 p212

メソッドと prototype

クラスのメソッドは prototype に定義されます。

class Product {
    #name: string;
    #price: number;

    constructor(name: string, price: number) {
        this.#name = name;
        this.#price = price;
    }

    getOrder(num: number): string {
        return `${this.#name} is ${this.#price * num}`
    }
}

const product1 = new Product('product1', 1000);
const product2 = new Product('product2', 2000);

console.log(typeof product1.getOrder, typeof product2.getOrder); // function, function
console.log(product1.getOrder === product2.getOrder); // true
console.log(product1.getOrder === Product.prototype.getOrder); // true
console.log(product2.getOrder === Product.prototype.getOrder); // true

// メソッドは prototype ベースなので Production.prototype.getOrder を Function.prototype.apply() で呼び出すこともできる
console.log(Product.prototype.getOrder.apply(product1, [2])); // 2000

console.log(typeof product1.getOrder); // function
// なので Function.prototype.apply() を使用できる
console.log(product1.getOrder.apply(product2, [2])); // this に production2 を指定しているので 4000 になる

TypeScript クラスを ES 5 にトランスパイル

// typescript
class Foo {
  public x: number;
  public echo: (x:number) => number;
  
  constructor(x:number) {
    this.x = x;
    // インスタンスメソッドを定義
    this.echo = x => x
  };
  // プロトタイプベースのメソッド定義
  double(): number {
      return this.x**2
  }
}

const foo1 = new Foo(3);
foo1.double();

ts.config の target を es5 にしてトランスパイルされたコード。

"use strict";
// JavaScript
var Foo = /** @class */ (function () {
    function Foo(x) {
        this.x = x;
        // インスタンスメソッドを定義
        this.echo = function (x) { return x; };
    }
    ;
    // プロトタイプベースのメソッド定義
    Foo.prototype.double = function () {
        return Math.pow(this.x, 2);
    };
    return Foo;
}());
var foo1 = new Foo(3);
foo1.double();

例外処理

例外とは、ランタイムエラーのことです。

プロを目指す人のためのTypeScript入門 p231

ランタイムエラー(例外)を発生させる

エラーを発生させたいとき、普通はまずエラーを表すオブジェクトを用意します。エラーを表すオブジェクトとは、Errorのインスタンスです。

出典:プロを目指す人のためのTypeScript入門 p231

finallyブロックの内容はエラーが発生してもしなくても実行されます。

出典:プロを目指す人のためのTypeScript入門 p238

(存在するオブジェクトの)存在しないプロパティにアクセス

  • 型に存在しない:コンパイル時の型チェック でエラーを発生させる
  • 実体(ランタイム)に存在しない: undefined を返す

※ 存在しないオブジェクトのプロパティにアクセスした場合は JavaScript はランタイムエラーを発生させます。
TypeScript は オプショナルチェイン を使ってオブジェクトが存在しない場合の処理をスキップできます。

高度な型

オプショナルチェイン

JavaScriptnullundefined なオブエクトに対するプロパティアクセスはランタイムエラーになります。

例: obj?.prop // または obj?.['age']

一方で obj?.prop の場合、obj が null や undefined の場合でもランタイムエラーは発生せず、結果はundefinedとなります。 出典:プロを目指す人のためのTypeScript入門 p260

?.から続く、まとめて飛ばされるひとまとまりの部分をオプショナルチェイン(optional chain)と呼びます。 出典:プロを目指す人のためのTypeScript入門 p263

型の絞り込み

  1. 等価演算子
  2. typeof 演算子
  3. switch 文

代数的データ型を(リテラル型を使って)ユニオン型で表す

プロを目指す人のためのTypeScript入門 p274

as による型アサーション

型アサーションは式 as 型という構文で、その式の型を強制的に変えるという意味です。 .... 型アサーションでは、実際の「値」に対しては何も怒らずに、TypeScriptコンパイラが認識する「型」だけが変化します。 出典:プロを目指す人のためのTypeScript入門 p286

上記より型アサーションに誤りがあるとランタイムでエラーが発生します。
なので「型アサーションの使用はできるだけ避けるべき」(出典:プロを目指す人のためのTypeScript入門 p286 )です。

型述語(ユーザー定義型ガード)

ユーザー定義型ガードとは戻り値に型述語が書かれた関数

ユーザー定義型ガード(user-defined type guards)とは、型の絞り込みを自由に行うためのしくみです。 出典:プロを目指す人のためのTypeScript入門 p299

ユーザー定義型ガードは、返り値の型として型述語(type predicates)が書かれた特殊な関数です。型述語には2種類の形があります。 出典:プロを目指す人のためのTypeScript入門 p299

// 出典:プロを目指す人のためのTypeScript入門 p299
function isStringOrNumber(value: unknown): value is string | number {
    return typeof value === "string" || typeof value === "number";
}

const something: unknown = 123;

if (isStringOrNumber(something)) {
    // ここではsomethingは string | number 型
    console.log(something.toString();
}

Apendix

  • 「JavaScriptではオブジェクトの存在しないプロパティにアクセスした結果はundefinedとなります」(プロを目指す人のためのTypeScript入門 p91)
  • 「日次データをプログラムで扱う際は(SO 8801 形式というフォーマットで扱うのが一般的ですが、Dateオブジェクトあこの形式にも対応しています。」(プロを目指す人のためのTypeScript入門 p123)
    • 1900-01-01T00:00:00+09:00 というフォーマット
  • プリミティブに対してプロパティアクセスを行うたびに一時的にオブジェクトが作られます(プロを目指す人のためのTypeScript入門 p129)
function foo () {
   //...
   return
       a + b
}
// return の後にセミコロンが追加さえるのでコンパイルエラーが発生します。

アンビエント宣言 (または環境宣言; ambient declaration)

ambient: 環境

JavaScriptコードを生成せず、型推論器にだけ情報を渡すのに使われるのが アンビエント宣言 (または環境宣言; ambient declaration) です。 declare のついた宣言がアンビエント宣言になります。 (declare なのに呼び名が「アンビエント」なのは不思議ですが、declareは「宣言する」という意味なので仕方なさそうです)

https://zenn.dev/qnighy/articles/9c4ce0f1b68350

Ref.