Closed37

なんとなくで書くTypeScriptからの脱却(基礎の基礎は書かない)

CODE-EGGCODE-EGG

never型

どんな値も入らない型。

let neverValue: never
const neverValue = "test" // エラー
const neverValue = 100 // エラー
const neverValue = null // エラー
const neverValue = undefined // エラー
CODE-EGGCODE-EGG

【利用シーン】
◆関数の戻り値
戻り値がない関数に利用する。voidとの違いとしてはnever型はreturnを書くとエラーになる。

const test = ():never => {
  return; // エラー
}

◆switch文(if-elseでも実現可)
次のような処理があったとします。

const arr: ["A", "B"] = ["A", "B"];
const index = Math.floor(Math.random() * 2);
const value: "A" | "B" = arr[index];
switch (value) {
  case "A":
    break;
  case "B":
    break;
  default:
    const neverVal:never = value;
}

default句のneverValはnever型でvalueもnever型になるのでエラーとなりません。
(case句でAかBであればbreakするためdefault句内はnever型になります。)
ここで、valueの型に"C"を足します。

const arr: ["A", "B", "C"] = ["A", "B", "C"];
const index = Math.floor(Math.random() * 2);
const value: "A" | "B" | "C" = arr[index];
switch (value) {
  case "A":
    break;
  case "B":
    break;
  default:
    const neverVal: never = value; // ここでエラー
}

すると、neverValがエラーになります。defalut句のvalueにマウスをホバーするとvalueはC型であることがわかりました。このようにnever型を利用するとcase句の追加忘れに気づくことができます。

CODE-EGGCODE-EGG

型エイリアス(typeによる型定義)

◎変数名は慣習的にアッパーキャメルケース(またはパスカルケースともいう)で記述する。
◎型エイリアスは名前の通りエイリアスであり変数ではない。ので再代入はできない。

type Human = {
  name: string;
}
Human = {
  age: number;  // 'Name' only refers to a type, but is being used as a value here.
// Name は型として参照されるだけですが、ここでは値として使われています。
}

◎必ずトップレベルで宣言する必要がある。関数内で宣言するとエラーになる。

CODE-EGGCODE-EGG

型エイリアスとinterfaceの違いは?

実はそんなに違いはなくどちらを利用しても問題はない。のでプロジェクト内でルールの統一がされていればどちらでもよい。
ただし等価ではない。
◎型の融合方法がことなる。
型エイリアスの場合

type Name = {name: string} ;
type Age = {age: number} ;
type Human = Name & Age

interfaceの場合

interface Name {name: string}
interface Age {age: number}
interface Human extends Name, Age {}

◎同名の型宣言
型エイリアスではエラーになる。
interfaceではエラーにならないでマージされる。

interface Human {name: string}
interface Human {age: number}
const human: Human = {
    name:"sato",
    age:20,
}
CODE-EGGCODE-EGG

型アサーション

asと書くことで実現できる。型推論を強制的に開発者側の型に置き換える。
これは、型の管理を開発者側で行う必要があるということ。
asを利用する際はその周辺の型定義を注意深く観察し管理しなければならないことに注意する。

CODE-EGGCODE-EGG

アサーションのでエラーが出る原因

アサーションするときにエラーになることがあります。これはアサーションされる変数の型とアサーションで指定した型が包含関係にないときに起きます。

const a:A = {name:"taro"};
const b:B = {age:20};
a = b as A; // エラー(Property 'name' is missing in type 'B' but required in type 'A'.)

また、string型とnumber型でもエラーになります。

let str = "A"
const num = 10;
str = num as string // エラー

これはnumber型はstrign型のもつ関数などを持っていないからエラーとなります。

解決方法

両社が包含関係であればアサーションが可能です。部分集合あればアサーションできます。

// 型エイリアス
type A = {
    name: string
    age: number
}
type B = {
    age: number
}

// bはaの部分集合なので両者は相互にアサーションすることができる。
let a:A = {name:"taro",age: 100};
let b:B = {age:20};

b = a as B // nameプロパティ
a = b as A // bが持たないnameプロパティを持つことになる
CODE-EGGCODE-EGG

型アサーションとキャストの違い

実は2つには明確な違いがあります。
型アサーションはコンパイル時の型チェックのときのみ有効で、ランタイムの挙動には影響を与えません。

CODE-EGGCODE-EGG

literal type widening

let変数へ値を代入すると抽象的な型になるという機能のことです。

const A = "A"; //A型になる
let B = "B" // string型になる

また、オブジェクトのプロパティに指定された場合は「literal type widening」が効く。

const A = "A"
const C = {
  A, // string型
}

このようにletやobjectのプロパティは抽象的な型へ変化します。実はこの2つには同じ特徴があります。どちらもミュータブルな値です。ですので、「literal type widening」はミュータブルな値に適用されると覚えましょう。

CODE-EGGCODE-EGG

ジェネリクス

動的な型付けを実現することができる。

CODE-EGGCODE-EGG

引数に対応して戻り値を変える関数を作成したいときジェネリクスは活躍します。

ジェネリクスを利用しない場合

ジェネリクスを利用しないで複数の型を定義する場合、次のようにユニオン型で引数を定義します。

// ジェネリクスを利用しない
const returnValueFunc = (value:string | number)  =>{
    return value
}

// 下記のどちらの値もstring | numberのユニオン型です。
const personName = returnValueFunc("taro");
const personAge = returnValueFunc(10);

これで、複数の型を受け取る関数を作ることができました。しかし、問題は戻り値です。
戻り値はユニオン型になってしまいます。
また、引数が増えたときに都度引数の型定義を書き換える必要があります。ジェネリクスはこれらの問題を解決します。

ジェネリクス

ジェネリクスを利用して先ほどの関数を定義すると次のように書けます。

// ジェネリクス  <T,U><第一引数の型,第二引数の型>
const returnValueFuncG = <T>(value: T)  => {
    return value
}
// personNameJ
const personNameG = returnValueFuncG("taro");
const personAgeG = returnValueFuncG(10);

アロー関数の頭に<T>と書くことでジェネリクスを利用することができます。"T"は受け取った引数の型に合わせて変化します。returnValueFuncG("taro");なので"T"はtaro型になるということです。戻り値は"T"より推論されるので、記載しなくてもOKです。

また、上は型推論によってpersonNameG は"taro"型になっています。constはリテラル型になるのでした。これをstring型にすることもできます。次のように"T"の型を引数と一緒に渡します。

const personNameG = returnValueFuncG<string>("taro");
const personAgeG = returnValueFuncG<number>(10);

これでpersonNameG はstring型になります。

CODE-EGGCODE-EGG

ジェネリクスの構文について

ジェネリクス基本的な使い方は以下の通り。

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

<T>と書いているがこれは任意に決められる。慣習的には以下のアルファベットの大文字が使われる。

  • T
  • S
  • U
    また、複数定義することもできる。
const returnValueFuncG = <T, S>(name: T, age: S)  => {}

配列の型定義

配列の型定義は次に3つがあります。

const arrA:string[] = [];
const arrB:Array<string> = [];
const arrC = new Array<string>();

Array<string>とありますが、これはジェネリクスです。つまり、TSの組み込みの機能として配列の定義が提供されているということです。

CODE-EGGCODE-EGG

ジェネリクスとextends

ジェネリクスは引数を受け取って初めて型が確定します。ですので、次のようなコードはエラーとなります。

type FF =  "タコス" | "たこやき" | "焼肉" |"らーめん";
const favoriteFoods:FF[] = ["たこやき","らーめん"]

const checkRamen = <T>(values: readonly T[]) => {
    const food = "らーめん"
    values.some((values) => values === food); // ここでエラー
}
const result = checkRamen<FF>(favoriteFoods);
console.log(result)

valuesはT型配列でfoodはstring型なのでエラーとなります。
これはasを利用してfoodをT型にできれば解決できそうです。
asは良くない方法のように思われますが今回は問題ないです。必ずvaluesの型は"T"になり、それを検査するときに利用するfoodも"T"であることが決まっているからです。checkRamen関数はそのための関数なので当たり前です。
しかし、この"決まっていること"を理解する機能はTSにはありません。ですのでasを使っても問題ないのです。

type FF =  "タコス" | "たこやき" | "焼肉" |"らーめん";
const favoriteFoods:FF[] = ["たこやき","らーめん"]

const checkRamen = <T>(values: readonly T[]) => {
    const food = "らーめん" as T // 次はここでエラー
    values.some((values) => values === food);
}
const result = checkRamen<FF>(favoriteFoods);
console.log(result)

今度はasでエラーが起きました。asのエラーは包含関係にないときにおこります。string型とT型のどちらも部分集合ではなかったようです。これを解決するためにextendsを利用します。

type FF =  "タコス" | "たこやき" | "焼肉" |"らーめん";
const favoriteFoods:FF[] = ["たこやき","らーめん"]

const checkRamen = <T extends string>(values: readonly T[]) => {
    const food = "らーめん" as T
    values.some((values) => values === food);
}
const result = checkRamen<FF>(favoriteFoods);
console.log(result)

これでエラーがなくなりました。<T extends string>ように書くことでT型はstring型を持つようになります。(string型がT型の部分集合となります。)。
T型は最低限string型を持っているのでアサーションすることができるようになります。

また、T extends stringとなったことで、T型でかつ最低限string型である配列しか受け付けなくなり、開発者から見れば何を渡すべきか明確になりました。

CODE-EGGCODE-EGG

typeof による型の取得

typeofを使ってみる型の見え方はJSとTSで異なる。

let str = "TypeScript";
let num = 100;
const arr = ["TS"];
const obj = {
    name:"taro",
    age:20,
}

//JavaScript(コンパイル→ランタイム結果)
console.log(typeof str); // string
console.log(typeof num); // number
console.log(typeof arr); // object
console.log(typeof obj); // object

// TypeScript(コンパイル時の型チェック)
type Str = typeof str; // string
type Num = typeof num; // number
type Arr = typeof arr // string[]
type Obj = typeof obj // {name:string, age:number}

流石TS。ちゃんと厳密な型になっています。一方JSは配列とオブジェクトがobject型になってしまっています。

CODE-EGGCODE-EGG

タプル型

固定長配列で要素の型が決まっている型。

let value:[string, number];

かなり厳密な型になっていて以下のような挙動になる。

let value:[string, number];
value = ["str", 100]; // OK
value = [100, "str"] // NG
value = ["str", 100, 100] // NG

上記のように要素の順番を入れ替えたり、多く入れたりすればエラーとなる。

CODE-EGGCODE-EGG

as const

as const とすることでliteral type wideningを無効化できます。
※literal type wideningについては本スクラップのアサーションのコメントにて説明あり。

const name = "taro"; // taro型
const obj = {name} as const  // {readonly name: "taro"}

しかも、nameプロパティがreadonlyになります。最強だ!!

CODE-EGGCODE-EGG

as const とタプル型

配列に対してas const を利用することでタプル型を作ることができます。

const arr = ["taro", 20] as const ; // ["taro", 20]型
CODE-EGGCODE-EGG

型の抽出

実は型定義から一部の型を抽出することができます。

type Human = {
    name: string,
    age: number,
}
type Name = Human["name"];

注意点としては、Human.nameのような書き方はできません。Human["name"]だけ有効です。

CODE-EGGCODE-EGG

配列の型抽出

次のようにして型の抽出ができる。

type Names = ["taro","ziro"];
type Taro = Names[0]; // taro型

また、配列の要素指定でnumberを利用すると配列の全要素の型を抽出してユニオン型として定義できる。

type Names = ["taro","ziro"];
type UinonNames = Names[number]; // "taro" | "ziro"型
CODE-EGGCODE-EGG

インデックスシグネチャ

オブジェクトのkey名を任意にできる型定義の機能。[key: 型]となり、key名は任意につけることができる。次のコードではkeyとなっているが本来は「lastnames」が良さそう。

type Human = {
    [key:string]:{
        name:string,
        age:number
    }
}

const human:Human = {
    "katou":{
        name:"tarou",
        age:100
    },
    "tanaka":{
        name:"ziro",
        age:100
    },
    "kimura":{
        name:"goro",
        age:100
    }
}
CODE-EGGCODE-EGG

インデックシグネチャの問題

keyを自由に定義できるようになったので、型定義としては緩くなってしまいました。
今回は"kato" | "tanaka" | "suzuki"以外のプロパティは弾きたいです。

Mapped Typesという機能を利用すれば解決できそうです。

type Lastnames = "kato" | "tanaka" | "suzuki";
type Human = {
    [key in Lastnames]:{
        name: string,
        age: number,
    }
}

const human:Human = {
    kato:{
        name:"taro",
        age:100,
    },
    tanaka:{
        name:"taro",
        age:100,
    },
    suzuki:{
        name:"taro",
        age:100,
    },
}

Mapped Typeの構文を一般化したものは次のようになります。

[key名 in T]: U

次のような書き方もできます。

type Lastnames = "kato" | "tanaka" | "suzuki";
type Info = {
    name:string,
    age:number,
}
type Human = {
    [key in Lastnames]:Info
}
Hidden comment
CODE-EGGCODE-EGG

webpack

モジュールバンドラー。モジュールバンドラーとは複数のファイルを1つのファイルにまとめる機能のこと。
ブラウザがHTMLの内容を画面に描画するとき、上からHTML読んでいきscriptタグが来たらサーバーと通信してJSの内容をとってきます。つまり、scriptタグが複数あるとリクエストが多くなり、処理に時間がかかります。ファイルを1つにまとめ通信を高速化することができます。

設定はwebpack.config.jsで管理される。

他にもいろいろできるみたい。。。調査

CODE-EGGCODE-EGG

tsconfig.json

tsの設定ファイル

CODE-EGGCODE-EGG

compilerOptions

TSからJSへコンパイルする際のルールや出力方法についての設定。

CODE-EGGCODE-EGG

※出力とはコンパイルによって作られるファイルのこと

target

出力するESバージョンを指定する。

module

出力されるモジュールの方式。ES2015(import/export)であれば基本的にOK。

strict

trueとすることでtsconfig.jsonに存在する複数の型チェック設定をすべて行う。
個別に設定することもできるが、strictを使えば1発で定義できる。

strictNullCheckes

null型とundefinde型は例えばstrignなどに代入できます。

const str: string = null // OK
const str: string = undefinde // OK
// 内部的には string | null | undefined と同じ型定義

strictNullCheckesをtrueとすることで代入ができなくなる。

const str: string = null // エラー
const str: string = undefinde // エラー
// ちゃんとstring型のみ受け入れるようになる。
CODE-EGGCODE-EGG

enum型

列挙型と呼ばれる型で「数値列挙型」と「文字列列挙型」の2つの書き方があります。

※enum型自体は非推奨の型定義らしいです。一応利用方法は確認したい。ということでenum型の機能を模倣した型定義も見ていきます。

CODE-EGGCODE-EGG

数値列挙型

上から順番に数字が振られていくので数値列挙型と呼ばれる。

// 数値列挙型
enum Color {
    Red, // 0
    Blue, // 1
    Yellow, // 2
}

let color:Color
color = Color.Red;
console.log(color); // => 0
console.log(Color.Red) // => 0
// 再代入
color = 1;
console.log(color); // => 1

const color2:Color = 2; 
console.log(color2); // => 2
CODE-EGGCODE-EGG

文字列列挙型

数値とは違って直接文字列を代入するとエラーになる

// 文字列列挙型
enum Color {
    Red="RED",
    Blue="BLUR",
    Yellow="YELLOW",
}

let color:Color = Color.Yellow;
console.log(color); // => "YELLOW"
// 再代入
color = Color.Blue;
console.log(color); // => "YELLOW"

// 文字列を代入
let color2:Color = "YELLOW" // エラー
CODE-EGGCODE-EGG

enum型の使いどころ

switch文で利用する。
型定義またはcaseのどちらか一方だけを修正するとエラーとなるため、型安全になる。

enum Color {
    Red="RED",
    Blue="BLUR",
    Yellow="YELLOW",
    Pink="PINK",
}

// エラーになる。
const checkColor = (color:Color) => {
    switch(color){
        case Color.Red:
        return "赤"
        case Color.Blue:
        return "青"
        case Color.Yellow:
        return "黄"
    }
}
CODE-EGGCODE-EGG

enum型はなぜ使うべきではないのか?

理由は2つあります。(これ以外もあるので enum typescriptでググるとたくさん出てくる)
1つは数値列挙型が型安全ではないからです。次のようなコードはチェックを通ってしまいます。

enum Color {
    "Red",
    "Blue",
    "Yellow",
}

const color:Color = 100;

もう一つはコンパイル後にenum型はjsファイルに残るからです。
実際に上のコードをコンパイルしたものが次にコードになります。
※下は数値列挙型になりますが、文字列列挙型でも同様です。

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Blue"] = 1] = "Blue";
    Color[Color["Yellow"] = 2] = "Yellow";
})(Color || (Color = {}));
const color = 100;

型定義とは異なり、値を持っているためランタイムに影響を及ぼします。

CODE-EGGCODE-EGG

enum型の代わりの型

keyof T を使う

type Color = {
    Red:string,
    Blue:string,
    Yellow:string,
    Pink:String
}

// keyof T でオブジェクトのプロパティのユニオン型を作れる
type Colors = keyof Color; // Colors型は Red | Blue | Yellow のユニオン型と同じ

const red:Colors = "Red";

console.log(red); // => "Red";

enum型で書いていたswitch文を置き換えると次のようになる。

// Pinkのcaseがないためエラーになる
const checkColor = (color:Colors) => {
    switch(color){
        case "Red":
        return "赤"
        case "Blue":
        return "青"
        case "Yellow":
        return "黄"
    }
}
CODE-EGGCODE-EGG

【実践】複雑な型

型は利用するオブジェクトから作成するとよい。オブジェクトに変更があった場合、追従して型定義も変更されるからだ。

複雑な型定義

const colors = {
    red:"RED",
    blue:"BLUE",
    yellow:"Blue",
} as const

type Colors = typeof colors[keyof typeof colors];

const red: Colors = "RED"; // OK
const pink: Colors = "PINK"; // エラー

type Colorsの型は現在"RED" | "BLUE" | "Blue" のユニオン型になっている。
なぜこのようになったのか1つずつ構文を見ていき解決していく。

as const

オブジェクトの型推論はliteral type wideningが働くことで抽象的な型定義がされる。as constとすることで厳密な型定義にしている。

typeof

typeofの後ろに置かれた変数の型を抽出する機能。

obj[ key名 ]

colorsオブジェクトに角かっこがついている。この構文は次のような挙動になる。

const obj = {
    name:"sato",
    age:20,
}

// obj[プロパティ名]でvalueを取り出せる
const age = obj["age"];

console.log(age) // 20

これはJSのオブジェクトの挙動になるが、解決したいコードはtypeof colors[]で型定義になっている。つまり、JSのオブジェクトではないが、型定義の場合も上記で示したJSオブジェクの挙動になる。また、jsでは["a"|"b"]のような書き方ができないことからも、typeof colors[ユニオン型]となっていて型定義であることがわかる。

keyof T

keyof の後ろにある、オブジェクト型のプロパティをユニオン型で取得する。

type Obj = {
    food:"ramen",
    age:20,
}

type UnionType = keyof Obj // food | age

const age:UnionType = "age"; // OK
const food:UnionType = "food"; // OK
const height:UnionType = "heith"; // エラー

結論

"RED" | "BLUE" | "Blue" のユニオン型にしたいなら次のような書き方ができる。

// typeof colors[プロパティ名のユニオン型];
type Colors = typeof colors["red"|"blue"|"yellow"];

このようなハードコーディングを回避するためにkeyofでユニオン型をとりだす。
ただし、keyof Tなのでkeyofの後ろは型でないといけない。
したがって次のような書く。

type tempType = keyof typeof colors;

これでtempType の型は "red"|"blue"|"yellow"となる。

CODE-EGGCODE-EGG

Assertion Functions

asserts x is Tと戻り値に書くことで、型を確定させる。
実例は次の通り。errorがスローされなければnumber型が確定する。

function assertIsNumber(x: unknown): asserts x is number {
  if (typeof x !== "number") {
    throw new Error("BOOM");
  }
}

// someValueはunknown型
const someValue: unknown = "foobar";

assertIsNumber(someValue);
// ここではsomeValueはnumber型

console.log(someValue.toFixed(1));

また、asserts x と書くこともできるが、この場合は引数はxはtruthyな値という意味になる。
注意点として、asと同じく型を開発者が決める仕組みになっている。また、throwするので呼び出し元でエラーハンドリングをする必要がある。

大変参考になる記事のコードを引用しました。この方のほかの記事も素晴らしすぎます。感謝永遠。。。
【参考URL】
https://qiita.com/uhyo/items/b8d2ea6fbf6214fc4194

CODE-EGGCODE-EGG

Conditional Types

type MyCondition<T, U, X, Y> = T extends U ? X : Y;
三項演算子と同様の記法。「TがUに代入可能であればXを、そうでなければY」という型を表す。

type UrlOf<T> = T extends {url:unknown} ? "url":never;

type Result = {
  message:string;
  url:string;
}

type Result2 = {
  message:string;
}

type ResultUrl = UrlOf<Result> // url型
type ResultUrl2 = UrlOf<Result2> // never型

【参考記事】
https://qiita.com/Quramy/items/b45711789605ef9f96de

CODE-EGGCODE-EGG

unknown型

any型と同様なんでも代入できる型。
異なる点としては、型アサーションされない限り利用できないので、anyより安全。
また、型がわからない値を受け取るときはunknownにしておけば、参照のみでアクセスは禁止であることがわかる。
【参考】
https://qiita.com/suzuki_sh/items/9b168b44d1d21c127aeb

このスクラップは2022/05/29にクローズされました