KDOC 554: 『Effective JavaScript』

この文書のステータス

  • 作成
    • <署名>
  • レビュー
    • <署名>

概要

なし。

メモ

  • すでに複数の異なるプログラミング言語を書き慣れている人にとっては新しい言語に取り組むさい、その言語特有の注意事項から学び始めるのは効果的である。それによって言語に対するメンタルモデルを鍛え上げるとともに言語の特徴を把握できる(まえがき)
  • TwoslashはTypeScriptのマークアップフォーマット
  • type Shape = Square | Rectangle の Rectangle は型だが、 shape instanceof Rectangle は値で、この場合はコンストラクタ関数を指す。型空間のシンボルと値空間のシンボルがある(p15)
    • よくわからない
  • 型エラーのあるコードからも出力が生成される。ほかの言語と異なるところ(p15)
  • 型演算は実行時の値に影響しない(p16)
  • 実行時の型は「宣言された型」と異なる可能性がある(p17)
    • TypeScriptの型なので、実行時には削除される
  • TypeScriptで実行時の型が宣言した型と一致しないと、混乱する。「不健全な型」という(p17)
  • C++のような言語の同じ名前でシグネチャが異なる関数が定義できる機能をオーバーロードという。TypeScriptではできない(p18)
  • 1つの関数には複数の型シグネチャを指定できるが、実装は1つでなければならない。関数のオーバーロード機能は完全に型レベルで存在する(p18)
    • どういったケースで使えるのだろう
  • 型は実行時に利用できない。実行時に型をチェックするには型を再構築するなんらかの方法が必要で、一般的にタグ付きユニオンやプロパティのチェックなどの方法がある(p19)
  • 関数を書くとき宣言したプロパティを持ち、それ以外のプロパティは持たない引数で呼び出すことを想定してしまいがちである。これは「閉じた型」「シールされた型」と呼ばれ、TypeScriptの型システムでは表現できない。TypeScriptの型は「オープン」である(p21)
  • Object.keys()が string[] で返ってくるのは「開いた型」であるから。オブジェクトのキーの一覧には、型宣言に明示的に列挙されていないフィールドが入っていることもありうるから、文字列で返る(p21)
  • JavaScriptがダックタイピングを奨励しているから、TypeScriptはこれをモデリングするため構造的型付けを用いる(p24)
  • エディタなどでTypeScriptがそれぞれの時点で変数の型をどのように捉えているかを見ることは、型の拡大と絞り込みに関する直感を養ううえで重要である。条件分岐のなかで変数の型が変化するのを確認することは型システムへの信頼を築くのにきわめて有効である(p30)
  • JavaScriptでは歴史的経緯から typeof nullobject" である(p32)
console.log(typeof null)
object
undefined
  • コードが実行される前にTypeScriptがエラーをチェックしているときには変数は型を持っているだけ。ありえる値の集合と考えられ、この集合は型のドメインと呼ぶ。number型はすべての数値の値の集合と考えられる。通常互換的に語られる「型」と「値の集合」は区別できる(p35)
    • never型は空集合。型階層の1番下に位置することから「ボトム型」と呼ばれることもある
    • 次に小さいのは単一の値を含む集合である。リテラル型が該当する
    • 2つまたは3つの値を持つ型はリテラル型のユニオンとして作れる
  • 型チェッカーが行っていることの多くは、ある集合が別の集合の部分集合であるかをテストすること(p36)
interface Identified {
  id: string;
}
  • ↑このインターフェースは、その型のドメインに含まれる値を説明している。どちらも満たすならそれはIdentified:
    • 値はオブジェクトか
    • stringに代入可能なidプロパティを持つか
  • 型を値の集合として考えると、型に対する演算を理解しやすくなる(p37)
  • 型演算は値の集合(型のドメイン)に適用されるのであって、インターフェースのプロパティに適用されるのではない(p37)
    • & のインターセクションは、プロパティではなく集合への交点である
  • extends は通常、interfaceにフィールドを追加するために使われるが、元の型の値の部分集合になっていればどんな使い方もできる(p38)
    • 元の型のフィールドは number | null なのを number に上書きするなど
  • 「サブタイプ」とは、ある型のドメインが他の型のドメインの部分集合であることを表現するための別の言い方である(p39)
  • TypeScriptは型の等価性をほとんどチェックしない。そのため型のテストを書くのが難しい(p42)
  • never (空の型)の対極にあるのは unknown である。この型のドメインにはJavaScriptのすべての値が含まれ、すべての型は unknown に代入可能である。型階層の最上位に位置するためトップ型と呼ばれる(p42)
  • 型アサーションも集合として考えられる。サブタイプであれば型アサーションできる。すべての型はunknown型のサブタイプであり、unknownを含む型アサーションは常に可能である(p52)
  • 型アサーションはいわゆる「キャスト」とは異なる。Cなどではキャストは実行時に値を変更できるが、型アサーションにはできない。型アサーションは型レベルの構文であり、実行時に消去される。値を変更するわけではない(p53)
    • キャストすると情報が失われる、みたいなことがあるが、そういうことはないことか
  • プリミティブはイミュータブルである点とメソッドを持たない点で、オブジェクトと区別される(p53)
  • stringプリミティブはメソッドを持たないが、Stringオブジェクトが定義されていて、それが代わりに使われている(p54)
x = "hello"
x.language = 'English'
console.log(x.language)
undefined
undefined
  • TypeScriptのラッパーオブジェクト型を使用しない。代わりにプリミティブ型を使う(p56)
interface A {
  name: string;
  count: number;
}
const r: A = {
  name: 'a',
  count: 1,
  elephant: 1, // 余剰プロパティはエラー
}

const obj = {
  name: 'a',
  count: 1,
  elephant: 1,
} // 推論される型の elephant は string
const r: A = obj; // OK. Aの elephant は any, obj の型のelephantは string. obj の型に含まれる値の集合はA型の値の部分集合である
const a: string | null = "a"
//    ^?
if (a !== null) {
   a
// ^?
}
const a: string | null (line 1)
const a: string (line 3)
const a: number[] = [1, 2, 3]
//    ^?
const b: readonly number[] = a;
//    ^?
const a: number[] (line 1)
const b: readonly number[] (line 2)
const b: readonly number[] = [1, 2, 3];
//    ^?
const c: number[] = b;
## Errors were thrown in the sample, but not included in an error tag

These errors were not marked as being expected: 4104.
Expected: // @errors: 4104

Compiler Errors:

index.ts
  [4104] 55 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
  • オブジェクトリテラルを既知の型を持つ変数に代入したり関数の引数として渡すと余剰プロパティチェックが行われる。エラーを見つける効果的な方法だが、型チェッカーが行う代入可能性チェックとは異なる(p60)
  • 「弱い型」とはオプションプロパティしか持たない型のことである。そのような型では代入可能性チェックをパスするためにプロパティが少なくとも1つ一致しなければならない(p60)
  • TypeScriptで関数式を使う利点は、パラメータや戻り値の型を個別指定する代わりに関数全体に対して1度に型宣言を適用できることである。ライブラリはよく使う関数シグネチャを型として提供する(p61)
// 重複
function add1(a: number, b: number) { return a + b; }
function sub1(a: number, b: number) { return a - b; }
function mul1(a: number, b: number) { return a * b; }
function div1(a: number, b: number) { return a / b; }

// クリア
type BinaryFn = (a: number, b: number) => number;
const add2: BinaryFn = (a, b) => a + b;
const sub2: BinaryFn = (a, b) => a - b;
const mul2: BinaryFn = (a, b) => a * b;
const div2: BinaryFn = (a, b) => a / b;
// 重複
function add1(a, b) { return a + b; }
function sub1(a, b) { return a - b; }
function mul1(a, b) { return a * b; }
function div1(a, b) { return a / b; }
var add2 = function (a, b) { return a + b; };
var sub2 = function (a, b) { return a - b; };
var mul2 = function (a, b) { return a * b; };
var div2 = function (a, b) { return a / b; };
  • type と interface のどちらを使うべきか。複雑な型の場合は type (型エイリアス) を使い、そうでない場合は interface を使うのがよい。interface には宣言のマージ、 type には型のインライン化がある(p71)
  • T[]readonly T[] よりできることが多いので、 T[]readonly T[] のサブタイプである。つまりミュータブルな配列はreadonlyな配列に代入できる。逆はできない(p75)
  • ざっくりとした型(Map<string, string)に対してデータ検証を行い、具体的な型を得るというパターンはTypeScriptではよくある(p90)
  • (感想)インデックスシグネチャがよくわからない。できるだけ避けたほうがよいのはわかった
type Vec3D = Record<'X' | 'Y' | 'Z', number>;
//   ^?

type StringNumber = Record<string, number>;
//   ^?
const value: StringNumber = { a: 1, b: 2, c: 3 };
type Vec3D = {
    X: number;
    Y: number;
    Z: number;
} (line 1)
type StringNumber = {
    [x: string]: number;
} (line 3)
const value: StringNumber (line 4)
const x = 'x';
//    ^?
let y = 'y';
//  ^?
const x: "x" (line 1)
let y: string (line 2)
const mixed = ['x', 1];
//    ^?
const mixed: (string | number)[] (line 1)
const arr1 = [1, 2, 3]
//    ^?
const arr2 = [1, 2, 3] as const
//    ^?
const arr1: number[] (line 1)
const arr2: readonly [1, 2, 3] (line 2)
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) {}
setLanguage('JavaScript')

type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) {}
let language = 'JavaScript'
setLanguage(language)
## Errors were thrown in the sample, but not included in an error tag

These errors were not marked as being expected: 2345.
Expected: // @errors: 2345

Compiler Errors:

index.ts
  [2345] 140 - Argument of type 'string' is not assignable to parameter of type 'Language'.
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) {}
const language = 'JavaScript'
//    ^?
setLanguage(language)
const language: "JavaScript" (line 3)
async function getNumber1() { return 42 }
//             ^?
const getNumber2 = async () => 42
//    ^?
const getNumber3 = () => Promise.resolve(42)
//    ^?
  • promiseにするのは重要なルールの強制のためである。関数には常に同期的に結果を返すか、常に非同期的に結果を返すかすべきで、状況によって同期的に結果を返したり非同期的に結果を返したりしてはいけない(p141)
function getNumber1(): Promise<number> (line 1)
const getNumber2: () => Promise<number> (line 2)
const getNumber3: () => Promise<number> (line 3)

関連

なし。