【技術書】プロを目指すtypescript入門を読んだので復習してみる

運営者:そうすけ


30代のブロガーとエンジニア。
工場勤務→情シス→ソフトウェアエンジニア
主にフロントエンド開発を行っています。

趣味はガジェットと植物

おはようございます!そうすけです。

現場で本格的にTypescriptを使うことになったので、一度書籍を読んで復習することにしました。

Uhyoさんという方がかかれた本です。かなりかみ砕いて書かれていて分かりやすい。

ガチ初心者でも1度読んだだけでなんとなく理解はできるのですが、完全に覚えて使いこなすのは難しかったので、辞書的に使っていこうと思います。

ところどころ引用タグで書籍の内容を一部抜粋しております。もし不備や意見などありましたらご連絡ください。

目次

最小限の環境構築

Typescriptをを動かすためには、サーバーサイドでJavascriptにコンパイルして実行する環境であるNode.jsが必要です。

mkdir typescript_blueberry //何でもよい
cd typescript_blueberry
npm init --yes //package.jsonつくる
npm install --save-dev typescript @type/node //typescript入れる
//----save-devというオプションは、インストールするパッケージがdevDependenciesである
//(プログラムの実行ではなくプログラムのビルドやその他開発時にのみ必要なパッケージである)ことを表します。
npx tsc --init  //tsconfingつくる

tsconfingの改変
"target": "ES2016", → "target": "ES2023",  //トランスパイルの程度
"module": "commonjs" → "module": "esnext" //コンパイラオプション

// "moduleResolution": "node"
→"moduleResolution": "node" 
npmでインストールしたモジュールをTypeScriptが認識できるようにするオプション

// "outDir": "./"
↓
"outDir": "./dist"
TypeScriptコンパイラによってコンパイルされた結果出力される.jsファイルが
出力される先のディレクトリを指定するコンパイラオプション

"include": ["./src/**/*.ts"]
srcディレクトリ以下のすべての.tsファイルがコンパイルの対象となります。

mkdir src //--scrにTSファイルを記載

npx tsc //コンパイルコマンド tscはTypescriptコンパイラーの略

dist以下にファイルができる 

npxとは

npxというのはnpmに付属するプログラムであり、node_modules内にインストールされたコマンドラインプログラムを実行してくれるツールのことです。

npm
プロジェクトで使用するパッケージが、どのパッケージのどのバージョンに依存しているのかを管理してくれるツール。

npx
Node.jsのパッケージを実行するツール。ローカルにないパッケージはネットから探してインストールする。
インストールが終了すると、インストールされていなかったコマンドやパッケージは削除する。

変数

文と式の違い

結果があるかどうか、で変わる!

文はただ値が入るのみで結果を返しません。
下記例なら、animalはdogですと言っているだけです。

式は値を計算して、結果が返ってきます。
下記例ならsumは2+6が返ってきているので、式になります。

//文
const animal:string = "dog";
console.log(animal)

//式
const sum :number = 2 + 6;

型注釈

const 変数: 型 = 式;

const greeting: string = "Hello, ";
const target: string = "world!";
console.log(greeting + target); 

letはつかわない

Javascriptでよく使用されているletですが、再代入が可能になるので読む人の負担が大きくなります。
基本的にConstで定数として扱うことで読み手に値の内容を保証させ、使わない方針です。

Typescript特有の型に関しても同様のことが言えて、型を書くことで後で書いた人が見えやすくなります。

プリミティブとは?

Typescriptが使用する原始的な値のこと!

String、Number、Boolean、BigInt、null、undefined、そしてシンボルがある。
よくプログラミング言語で出てくる奴です。

Javaと違って数字は小数(float)はなく、number型になる!

リテラルとは?

値を生みだすための式のこと!

数値を得るための式
const binary = 0b1010;    // 2進数リテラル
const octal = 0o755;      // 8進数リテラル
const hexadecimal = 0xff; // 16進数リテラル

文字列を得るための式
const str1: string = "Hello"; // 文字リテラル
const str2: string = 'world!'; // 文字リテラル
console.log(str1 + ", " + str2); // "Hello, world!" と表示される

// テンプレートリテラル
console.log(`${str1}, ${str2}`); 

真偽値を得るための式
const no: boolean = false; // 真偽値リテラル
const yes: boolean = true; // 真偽値リテラル
console.log(yes, no); // true false と表示される

nullとundefine
const n: null = null;
const u: undefined = undefined;

オブジェクト
//オブジェクトリテラル
const obj = {
  foo: 123,
  bar: "Hello, world!"
};

nullよりundefinedに注意すればよい

【null】も【undefined】も値がないということを表すのに有用なのですが、若干目的が異なります。

特徴undefinednull
意味値がまだ設定されていない値がないことを明確にしている
誰が設定するかJavaScriptが自動で設定する開発者が手動で設定する

nullよりundefinedに注意すればよいです。
なぜなら、TypescriptにはNullに対するサポートが手厚く、型を指定しなくても暗黙の型変換で対応してくれる機能があるからです。

例えば、数値型変換したときnullだと暗黙の型変換がなされ、0になりますが、undefinedだとNanとなり暗黙の型変換が機能しません。

//nullとundefinedの暗黙の型変換
const num3 = Number(null);
console.log(num3); // 0 と表示される

const num4 = Number(undefined);
console.log(num4); // NaN と表示される

//Boolean型の暗黙の型変換
const num1 = Number(true);
console.log(num1); // 1 と表示される

const num2 = Number(false);
console.log(num2); // 0 と表示される

この場合、型エラーやバグが起きやすくなるので、undefinedには注意。

オブジェクトについて

オブジェクトとは連想配列のこと!

constという定数で宣言するが、プロパティの内容の書き換えはできる!

基本的な構文

//オブジェクト
const user = {
  name: "uhyo",
  age: 25,
};

// エラー: Cannot assign to 'user' because it is a constant.
user = {
  name: "John Smith",
  age: 15,
};

//プロパティの書き換えはOK
user.name = "manabe"

//スプレッド構文で展開できる
const sexuser = {
	...user,
	sex:"femail"
}

注意点:オブジェクトそのものをラップしたのか、コピーしたのか

意図せぬところでプロパティが変わるので、オブジェクトをコピーするときは展開すること!


//下記はラップ(コピーされていないので)
const userA = user
userA.name = "papa" //これでuserのプロパティも書き換えられてしまう

//下記のようにスプレッドを使えば、プロパティを新たに宣言したと同様なのでコピーされる
const userB = {...user}

オブジェクトの型定義の方法

//型推論を前提にいきなりプロパティに代入する方法
//これだとobjが{foo:string,bar:number}という方で自動的に保管される
const obj = {
	foo:"hello",
	bar:123
}

//型推論でも別の方を入れると型エラー
obj.foo = true

//一般的には明示的にオブジェクト型を定義したほうがやさしい
//type分を使う
type FooBarObj = {
	foo:number;
	bar:string;
}

//オブジェクト定義された型で宣言する
const obj:FooBarObj = {
	foo:"hello",
	bar:123
}
//これでもOk
const Mypra:Myobj = {foo:"manabe",bar:123} 



//型定義と同時に代入処理を記載することも可能
const obj:{foo:string; bar:number;} = {foo:"hello",bar:123}

//interfaceでもオブジェクトを宣言可能。
//ただしオブジェクト型の宣言のみにしか使用できないので、typeがよくつかわれる
interface FooBarObj {
	foo:string;
	bar:number;
}

//オプショナルなプロパティ(あってもなくてもOK)は ? を付ける
type Myobj = {
	foo:string;
	bar:number;
	baz?:number;
}
// undefined と表示される。bazはnumber|undefined のユニオン型
console.log(obj.baz); 

//Readonlyでオブジェクトのプロパティの再代入を防げる
type Myobj = {
 readonly foo :number;
}

部分型

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

このような前提条件の場合、型Sは型Tの部分型になります。

部分型
//この例でいうHumanはAnimalの部分型である
type Animal = {
	age: number;
}

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

const ManabeMan:Human = {
	age: 32;
	name: "manabe";
}

//human型で宣言した変数が、Animal型の変数に代入できる
//HumanはAnimalの部分型なのでこれが成り立つ
const ManabePet:Animal =  ManabeMan

//この例もHumanFamilyはAnimalFamilyを包括しているで
//HumanFamilyはAnimalFaimilyの部分型である
//familyNameの型が同じなのと、HumanはAnimalの部分型だから
type AnimalFamily = {
  familyName: string;
  mother: Animal;
  father: Animal;
  child: Animal;
}

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


型引数

定義したオブジェクト型として使用するときにプロパティの型を決める方法!
型引数を持つ型はジェネリック型と呼ぶ。


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

const ManabeFamily:Family<string,string> = {
    mother:"mie",
    father:"yasusi",
    child:"sou"
}

型引数の制約

//extendを付けるとその型引数は常にextendした型の部分型でないといけない
type HasName = {
    name: string;
};
type HasNameFamily<Parent extends HasName,Child extends HasName> = {
    mother:Parent;
    father:Parent;
    child:Child;
}
//この例で言えばHasNameFamily型を持つオブジェクトに代入するときは
//必ずname:stringの型を持っていないといけない
type People = {
    name:string;
    sex:string;
}
const nameFam:HasNameFamily<People,People> = {
    mother:{name:"mie",sex:"female"},
    father:{name:"yasusi",sex:"male"},
    child:{name:"sou",sex:"male"}
}

配列

//配列型
//表現が2種類あるがどっちも同じ
Array<T>
T[]
//簡単な場合
const arr1:string[] = ["manabe","kochu"]
//複雑な場合は型引数に
const arr2:Array<{name:string}> = [{name:"kochu"},{name:"manabe"}]

//書き換え付加な型で定数にもできる
const arr3: readonly number[] = [1,10,100]
arr3[0] = 1 //書き込みエラーがでる

//要素にアクセスるときはできるだけfor of を使用する
for(const num of arr3){
    console.log(num)
}
//配列を取り出すときはインデックスアクセスはできるだけしない
//なぜなら存在しないインデックスにアクセスしてもエラーはでない
const num:arr3  = arr3[100] //undefinedとなるだけ

分割代入

非常によく使われる

プロパティにアクセスするのはなく、変数にいれて扱った方が吉

type Drink = {
    label:string;
    price:number;
    kinds:string;
    shape:{
        capShape:string;
        bottleShape:string;
    }
}

const JapaneseTee:Drink = {
    label:"緑茶",
    price:100,
    kinds:"tee",
    shape:{
        capShape:"circle",
        bottleShape:"500ml"
    }
} 

//プロパティと同じ変数名で代入できる
const {label, price} = JapaneseTee

//プロパティ名と違う変数に分割代入したいときは、【プロパティ名:変数名】で表現
//型注釈はできない、同じ型に型推論される
const {label:labelname, price:kakaku} = JapaneseTee

//デフォルト値を入れることもできる
//★初期値ははオブジェクトのプロパティが「undefined」のときのみ適用される

const {label:labelname = "ayataka", price = "600"} = JapaneseTee

//ネストされたオブジェクトはプロパティ名:パターンの気泡で取り出す
const {kinds,shape:{capShape}} = JapaneseTee
//...〇〇で宣言せずに残りのオブジェクトを入れることもできる
const {label,...restObj} = JapaneseTee

関数

書き方 
1:関数宣言(分)
//function 関数名(引数:引数の型):返り値の型{中身の処理}
function range(min:number,max:number):number[]{
    const result = []
    for(let i = min; i < max; i++){
        result.push(i);
    }
    return result;
}

//中身がないときはvoid
function sayHello(n:number):void {
    for(let i = n; i < n; i++){
        console.log("hello");
    }
}

2:関数式(式)
//関数を変数に入れて関数を宣言する
type Human = {
    weight:number;
    height:number;
};

■function関数式
const calcBMI = function(human:Human):number  {
    const BMI:number = human.weight / human.height ** 2
    return BMI
};
//分割代入することもできる
//この場合の引数はhumanオブジェクトで、使用するプロパティはweightとheight
const calcBMI_b = function({weight,height}:Human):number  {
    const BMI:number = weight / height ** 2
    return BMI
};

■アロー関数 
//こちらの方がよくつかわれる
const calcBMI_allow = ({weight,height}:Human):number => {
    const BMI:number = weight / height ** 2
    return BMI
};

//アロー関数の省略形 
//★これめっちゃでてくる
//特にコールバック関数で使用される

//関数内の式は1つのみしか書けない、単純な処理のみ
//かっことreturnがない
const calcBMI_short = ({weight,height}:Human):number => weight / height ** 2;

//オブジェクト自体を処理に書く場合、{}が必要
type HumanBMI = {
    bmi:number;
}
const calcBMI_short = ({weight,height}:Human):HumanBMI => ({
    bmi:weight / height ** 2
}) 

■メソッド記法 オブジェクトのプロパティとして宣言
//プロパティとしての関数をメソッドと呼ぶ
const obj = {
    double(num:number):number{
        return num * 2
    }
}
obj.double(100)

関数の型

関数型は**【(引数リスト)=> 返り値の型 】**というアロー関数のような形を持っている

VSCODEでカーソルを与えると出てくる


//下記関数の型は (suzi:number) => string 型
//引数名は型に影響しない
const N_repeat = (suzi:number):string => "N".repeat(num)
//戻り値がない場合はvoid型を戻り値とする
const voidNone = (num:number):void => console.log(num)

//★返り値の型注釈は省略もできる★
//ただし明示的に書いた方が型チェックの恩恵を受けられるので使うべき
//省略すると型推論で型チェックするが、それが意図したものなのか間違いなのかわからない
const N_repeat:N = (num:number)=> "N".repeat(num)
const voidNone = (num:number) => console.log(num)

//関数型の部分型も存在する
//例えば、関数型Aの引数が関数型Bの関数型の引数の部分型、であれば関数の部分型として使用できる
//戻り値も同様

ジェネリクス関数

いやというほど出てくる、型引数を用いた関数

関数処理の中に型引数が存在するときに使用する。

//文字の配列、数字の配列を使う時に決める関数
function nandemoArray<T>(element:T,length:number):T[]{
    const result:T[] = []
    for(let i = 0;i < length;i++){
        result.push(element)
    }
    return result
}
//関数を使うときに肩を決められる
nandemoArray<string>("a",3) //型はnandemoArray<string>(element: string, length: number): string[] 型になる
nandemoArray<number>(2,3) //型はnandemoArray<number>(element: number, length: number): number[] 型になる

//ジェネリクス使用した関数式の書き方
//「型引数Tを持ち、T型の引数とnumber型の引数を受け取ってT[]型の値を返す関数」

//function式
const nandemoArray_f = function<T>(element:T,length:number):T[]{
    const result:T[] = []
    for(let i = 0;i < length;i++){
        result.push(element)
    }
    return result
}
//アロー関数
const nandemoArray_a = <T>(element:T,length:number):T[] => {
    const result:T[] = []
    for(let i = 0;i < length;i++){
        result.push(element)
    }
    return result
}
//メソッド記法
const nandemoArray_o = {
    nandemoArray<T>(element:T,num: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] => {
    return [left,right]
}
const p = pair<string,number>("manabe",30)

//型引数に必ずname:stringを含む部分が他のオブジェクトを設定したい場合
//型引数としてextendでオブジェクトを定義する
const nandemoArray_ex = <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 Pet = {
    name:string;
    age:number;
};

console.log(nandemoArray_ex<Pet>({
    name:"udon",
    age:4
},3))

//関数を呼び出す際は、引数で型推論されるので型引数は省略できる
//実際はほとんどこれで、これが利便性を向上させている
const p = pair("manabe",30)
console.log(nandemoArray_ex({name:"udon",age:4},3))

演習:0か1か検証する関数

重要なのがcallbackの引数がArrayで指定した型で決まるということ。

T型の配列なので、コールバック関数の引数はTであると関数型を定義できるか

//ジェネリクス使ったmap関数作ろう
const j_map = <T,U>(array:T[],callback:(x:T)=>U):U[] => {
    const return_array:U[] = [];
    for(const i of array){
        return_array.push(callback(i))
    }
    return return_array;
}

const data_tako = [1, -3, -2, 8, 0, -1];
const result_tako: boolean[] = j_map(data_tako, (x) => x >= 0);

クラス

前提

クラスの考え方は言語によって異なる。Typescriptはオブジェクト型を作って、そこにオブジェクトを作ることが多いです。
そのため必ずクラスが必要にならなく、出番が多くないことが多いです。

ミュータブル(型はある程度決まってるけどプロパティが変わりやすい)な使い方がされるオブジェクトやメソッドに便利な手段として使用させる。

メリット

  • ①newという統一的な構文でオブジェクトの初期化を行える
  • ②値の名前空間と型の名前空間にまたがった名前を定義できる

Typescriptでは、変数名の名前空間と、型名の名前空間が異なる。
だから下記のように型名と同じ変数に入れることが可能

Itemという変数名と、型の名前が一致しています。
(下記例は、可読性としてはやるべきではない)

// 型名の名前空間にItemを作成 
type Item = { name: string; price: number; } 
// 変数名の名前空間にItemを作成 
const Item: Item = { name: "りんご", price: 200 }; 
// このItemは型名のItem 
const orange: Item = { name: "みかん", price: 150 }; 
// このItemは変数名のItem 
console.log(Item);

クラスの基本

//クラス
class User {
   name:string = "";
   age:number = 0; 
	//メソッドはメソッド記法
   isAdult():boolean{
    return this.age >= 20;
   }

   setAge(newAge:number):void{
    this.age = newAge
   }
}

//インスタンス化
const Sou = new User();
Sou.age = 31;
const isAdult:boolean = Sou.isAdult()
Sou.setAge(20)

//オプショナルもできる
class User_option {
    name?:string;
    age:number = 0; 
 }
 
 修飾子とコンストラクタ
 //コンストラクタ付きクラス。
 
 class User_constract_obj <T>{

  //静的プロパティ。クラスそのもののプロパティ。staticを使う 
    static userAdmin:string = "sousuke";
    static userAdminAge:number = 100;
    
 //コンストラクタある場合、プロパティには初期値を代入しない
    name:string; //何もかかないとpublic 修飾子が付くのと同義
    readonly age:number;
    data:T; //コンストラクタでプロパティの型引数も可能になる
    
	//メソッドはreadonlyやprivateでもアクセスできる
    constructor(name:string,age:number,data:T){
        this.name = name;
        this.age = age;
        this.data = data; //型引数を使用した
    }
 }
 
//このようにpublicを書いてプロパティ宣言とコンストラクタを同時にすることも可能
class user_constract_obj1{
    constructor(public name:string,private age:number){}
}

const Sou_constract = new User_constract_obj("sousuke",23)

//クラスオブジェクトに直接アクセス
User_constract_obj.userAdmin

//インスタンスからはアクセス不可でエラー
Sou_constract.userAdminw

//private 
//これは内部からしかアクセスできなくなる。
//なのでSetAgeやisAdult経由でしかアクセスできないので、安全になる
class User_Sou {
   private age:number = 0;
   //「#」で書いてもprivateと同義
   //厳密にいうと#はJSの機能なので、コンパイルがもし通ってしまってもprivateの機能として働く
   #age:number = 0; 

   isAdult():boolean{
    return this.age >= 20;
   }

   setAge(newAge:number):void{
    this.age = newAge
   }
}

//publicを書いてプロパティ宣言とコンストラクタを同時にすることも可能
class user_constract_obj1{
    constructor(public name:string,private age:number){}
}

「protected」という子クラスからのみ親クラスのプロパティにアクセスできる修飾子もある、でも結局子クラスから書き換えられてしまうので実装すべきでない。

基本privateとpublicで実装!

クラスの型

クラスの方はクラス宣言した型名になる。下記例だとUser_Sou型である

class User_Sou {
    name:string = "";
    age:number = 0; 
 
    isAdult():boolean{
     return this.age >= 20;
    }
 
    setAge(newAge:number):void{
     this.age = newAge
    }
 }

クラス宣言はこの2つの意味がある。

  • クラスオブジェクトという値(が入った変数)
  • 同名の型を同時に作成する構文

正確に言うと、クラス宣言というのは両方の名前空間に同時に名前を作成する構文

例えばUser_Souクラスをインスタンス化した変数は、「User_Sou」型となる

継承

継承
//親クラス
class standardUser{
    name:string;
    age:number;

    constructor(name:string,age:number){
        this.name = name;
        this.age = age;
    }
    
    public isAdult():boolean {
        return this.age >= 20;
    }
    
}

//extendsで子クラスにする
class premiumUser extends standardUser{
    //子クラス特有のプロパティ
    rank:string = "gold";
    
    オーバーライド
    //親クラスのプロパティやメソッドを上書き(オーバーライド)できる
    //親クラスの既存機能の挙動を変えたいとき
    //ただし関数型が一致するときのみ(ここでは()=>boolean型)
    public override isAdult(): boolean {
        return true;
    }
    //「override」を省略もできるが、絶対書いた方がいいと思う
    public isAdult(): boolean {
        return true;
    }

}

//継承した子クラス作成 
//ちなみに子クラス辞退にコンストラクタが必要ない場合は
//勝手に親クラスのコンストラクタが使用される
const Sousuke = new premiumUser("sousuke",31)

//standerdUser(親クラス)のプロパティにアクセスできる
console.log(Sousuke.age)
//子クラスのプロパティももちろんアクセスできる
console.log(Sousuke.rank)

//もちろん継承された子クラスは親クラスの部分型になる
//例えばstandardUser型の引数に
//premiumUser型のオブジェクトを引数に取ることができる
const helloUser = (u:standardUser) => {
    return `こんにちは。${u.name}さん`
}
const greedName = helloUser(Sousuke)

子クラスのコンストラクタ
class premiumUser extends standardUser{
 
    //子クラス特有のプロパティやメソッドにコンストラクタ使う場合
    rank:string;
    
    //子クラスでコンストラクタを使う場合は super が必要
    //まず親クラスのコンストラクタを動かすよ、という宣言を明示的に行う
 
    constructor(name:string,age:number,rank:string){
        super(name,age);
        this.rank = rank

    }
    
    public isAdult(): boolean {
        return true;
    }

}
const Sousuke = new premiumUser("sousuke",31,"gold")

オーバーライドは親クラスの依存性が強すぎるので、実装するときには要注意。

例えば、「isAdult」を「isChild」に親クラスで変更したら、オーバーライドした関数は動かなくなってしまう

なので「override」を書くと、子クラス側のコンパイルエラーで止まってくれる。せめて記述しよう

例外処理

※あまり使用しない

ランタイムエラーで止まってはいけない処理を記述する。動かすためにこれを多用するのはよくなく、通常try内を適切に処理をする方向性。

catchには必ずログを残す。

try {
    console.log("エラー発生させます");
    throwError();
    console.log("エラー発生後");//ここは実行されない
} catch (error) {
    console.log("エラーをキャッチしました");
    console.log(error);
}

function throwError(){
    //エラーオブジェクトをインスタンス化
    const error = new Error("エラー発生しました!!!!");
    //throw 式 で実行する式を指定する
    throw error;
}

こういった例外処理は、Typescriptだと、undefinedを返す方が、型システム的に使いやすい

function getAverage(nums: number[]) {
  if (nums.length === 0) {
    return undefined;
  }
  return sum(nums) / nums.length;
}

なぜなら、catchのerrorオブジェクトはunknown型で何が入るか全くわからなく、危険である。

大域脱出したいとき(処理を途中で抜けたいとき)に使う目的もあるが、returnで対応すれば?という声もある。

だが、基本的にエラーで抜けた後の処理を記述できるのはtry-catchだけで、明示的にcatch内のエラーの処理を統一にまとめたいときはTryCatchをつかうべし。

他finally分もある。例えば、エラーであってもなくてもtrycatchの最後に実行される処理に使用する。
(例:ファイル処理のファイルのクローズなど)

TS特有の機能

ここが結構肝になってくる

union型とインターセクション型

union型:「または」
〇型または△型という風に型設定できる

type Animal = {
    species:string;
    name:string;
}
type Man = {
    species:string;
}
type unionUser = Animal | Man

//いくらユニオン型でも存在しないプロパティにはアクセスできない
const getName = (user:unionUser):string => {
    return user.name;
}

インターセクション型;「かつ」
オブジェクトを拡張した新しい方を作る用途で使用
//インターセクション型
type Human_inter = Animal & {age:number}

const hu_in:Human_inter = {
    species:"human",
    name:"manabe",
    age:23
}

unionUser型をを持つことは、Animal型かMan型かなのか、使用するまで不明ということ

オプショナル

どれかわからないのであれば、オプショナルでいいのでは?という考え方がありますが、下記のように分けるべきです。

  • undefined:「プロパティの値が存在しないないこと」
  • union型:「プロパティ自身が型上に存在しないこと」

これらは明示的に分けるべき

■オプショナルなプロパティ
このときのageは結果的に、number|undefind型である

type Takagi = {
    name:string;
    age?:number;
}

■undefinedのユニオン型
下記は意味が違う
この型はageプロパティを持たないと型エラーとなる
type Takagi_undfined = {
    name:string;
    age:number|undefined;
}

★使い方:省略を許容するかどうか
「データだけないかもしれない可能性」を表現したいだけの場合に向いている。
例えば、上の例のオプショナルだと、プロパティが「存在しない」のか「値がない」のかわからない
プロパティの省略に特に強い動機がない場合はユニオン型の方が型エラーとなるので安心

オプショナルチェイニング

通常ならプロパティにアクセスできないが、「?」を付けることで存在したらアクセスできて、存在しない場合はundefinedにする

const useMaybyAnimal = (animal:Animal|undefined):void => {
    const name = animal?.name;
    console.log(name)
}

通常のobj.propというプロパティアクセスは、objがnullやundefinedである場合は使用できません。

一方で、obj?.propの場合、objがnullやundefinedの場合でもランタイムエラーは発生せず、結果はundefinedとなります。

プログラムによってはHuman | undefinedのように「nullやundefinedかもしれないオブジェクト」を扱う機会が多くあります。

「Humanだったらプロパティにアクセスするが、undefinedだったらアクセスしない」という取り扱いは頻出です。

関数呼び出しの場合

//関数自体が存在すれば実行(関数呼び出しオプショナル)
//関数が与えられているときのみ呼び出したい
//用途:ボタンなどのUIコンポーネントに仕込む関数など
type GetTimeFunc = () => Date;
const useTime = (getTimeFunc:GetTimeFunc|undefined):void =>{
    //引数として関数が存在したら実行する
    const timeOrUndefined = getTimeFunc?.();
}

メソッド呼び出しの場合

//オブジェクトが存在すれば実行(メソッド呼び出しオプショナル)

//例:ログインユーザーの判定
//loginuserがnullなら結果undifined→(真偽値に変換するとfalseに変換される)
//大人でなければfalse

type loginUser = {isAdult():boolean}
const checkFuncAdultUser = (loginuser:loginUser|null):void =>{
    if(loginuser?.isAdult()){
        console.log("オトナのコンテンツ見せて上げる")
    }
}

この例ではUserはisAdultメソッドを持つオブジェクトです。

関数checkForAdultUserは、Userを受け取るかもしれないし受け取らない(nullが与えられる)かもしれません。それに対し、この関数はuserが存在してかつuser.isAdult()を満たす場合にのみ処理を行います。

このような処理は、「ログインしていないユーザー」「ログインしているユーザー(未成年)」「ログインしているユーザー(成年済)」のような区分を扱う場合に発生しがちです。

この例では、ログインしていないユーザーはnullで表され、ログインしているが未成年のユーザーはisAdult()がfalseを返すUserオブジェクトで表されます。

成年済のログインユーザーはisAdult()がtrueを返すUserオブジェクトですね。

この例は、与えられたuser(User | null型)がこの3パターンのうち最後のものであることを一発で判定するためにuser?.isAdult()という式を用いています。

これは、userがnullでなければtrueかfalseになる一方で、userがnullならば結果がundefinedとなります。

すでに学習したように、undefinedは真偽値に変換するとfalseになりますから、if文を通過できるのはuserが存在してuser.isAdult()がtrueを返す場合だけなのです。

ちなみに、「未成年ユーザーのみ」といった判定もuser?.isAdult() === falseとすれば判定可能です。

~型の絞り込み~

リテラル

//おもにリテラル型のユニオン型で使用されることがTSで多い
//いくつかの特定の値のみを受け付けたいという場合に
//ユニオン型とリテラル型の組み合わせが非常に適している
type SignType = "plus" | "minus";

引数リテラル
書き方①
const plusOrMinus = (hugou:"plus"|"minus") : number => {
    return hugou === "plus" ? 1 :-1;
}
書き方②
const plusOrMinus = (hugou:SignType) : number => {
    return hugou === "plus" ? 1 :-1;
}

型の絞り込み

ユニオン型が実際に持つ方によって、型情報が絞られ、処理を変えることができる
//型の絞り込みをしないと使えない関数がある(plusOrMinusの引数はSignType型のみ)
//与えられた値が特定の方のみ処理を行うことが可能
//投下演算子を用いるベーシックな絞り込み

const numberWithSign = (num:number,sign:SignType|"none"):number => {
    if(sign === "none"){
        return 0;
    }
    else{
        return num * plusOrMinus(sign);
    }
}

typeof演算子
//typeof演算子でも可能
const formatNumberorString = (value:number|string):(number|string) => {
    if(typeof value === "number"){
        return value * 3;
    }
    else{
        return value;
    }
}

比較演算子を判断し、型をコンパイラが絞ってくれる(VSCodeで確認)

タグ付きユニオン(直和型・代数的データ型)

プロパティの文字列で型を絞る「タグ」という考え方もある

(代数的データ型ともいう)

タグで型を絞り込むことによって、TSがプロパティにアクセスできるようになる方法

※きわめて基本的な設計パターンらしい

//tagというリテラル型(指定の文字列しか入らない型)で
type Animal = {
  tag: "animal";
  species: string;
}
type Human = {
  tag: "human";
  name: string;
}
type Robot = {
  tag: "robot";
  name: string;
}
type User = Animal | Human | Robot;

function getUserName2(user: User): string {
  switch (user.tag) {
    case "human":
      return user.name;
    case "animal":
      return "名無し";
    case "robot":
      return `CPU ${user.name}`;
  }
}

lookup型

プロパティの型を参照して追従する型

age型を変更しても関数の引数を変えなくていい(あまり使わない方がいい)

type Human = {
  type: "human";
  name: string;
  age: number;
};

function setAge(human: Human, age: Human["age"]) {
  return {
    ...human,
    age
  };
}

keyof型

プロパティ名の文字リテラル型のユニオン型

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

type HumanKeys = keyof Human; //型が"name"|"age"型になる

let key: HumanKeys = "name";
key = "age";
// エラー: Type '"hoge"' is not assignable to type 'keyof Human'.
key = "hoge";

!記法

式!のように書く構文で、式がnullまたはundefinedである可能性を無視するという意味

function showOneUserName(user1?: Human, user2?: Human): string | undefined {
  if (user1 === undefined && user2 === undefined) {
    return undefined;
  }
  if (user1 !== undefined) {
    return user1.name;
  }
  return user2!.name;
}

型アサーション(as)

型を上書きする方法

function getFirstFiveLetters(strOrNum: string | number) {
  const str = strOrNum as string;
  return str.slice(0, 5);
}

// "uhyoh" と表示される
console.log(getFirstFiveLetters("uhyohyohyo"));

// ランタイムエラーが発生!
console.log(getFirstFiveLetters(123));

※TSの型推論システムを意図的に破壊するのであまり使わない方がいい

プログラマがTSの型システムをうまく使えず、無理やりコンパイラを黙らせるために行うことが多い

any型

TSの型チェック機能を無効化する型

何を入れてもコンパイルエラーが発生しない型なので、最凶の危険性をもつ

//any型
const doWhatever = (obj:any) => {
    //好きなプロパティにアクセス
    console.log(obj.user.name);
    //関数もOK
    obj();
    //計算もOK
    const result = obj * 3;
    //文字型もOK
    const moji:string = obj;

}
//コンパイルエラーがでないが必ずランタイムエラー
doWhatever("a")

※anyは「完全にチェックを放棄する」機能である一方でasやユーザー定義型ガードは「正しい状態をTypeScriptに教える」機能

unknown型

anyとほぼ同じだがプロパティアクセスを認めない

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

引数の型を絞り込む機能 isStringOrNumberの戻り値は真偽値だけど、戻り値がtrueだった場合 引数で合ったsomethingにstring|number型が付与される

※これも人間の手で型を指定する方法のため、型安全性を損なう危険な機能

一定以上複雑な型を絞り込ませるときに使用する

※anyやasを使用するならこちらを使って型を指定すること、ロジックを組むだけ安心

const isStringOrNumber = (value:unknown):value is string|number => {
    return typeof value === "string" || typeof value === "number";
}
const something:unknown = 232;

if(isStringOrNumber(something)){
    //ここでsomething_numはstring|number型
    console.log(something.toString());
}
//ユーザー定義型ガード(型述語)

//sometingの型を絞り込む機能
//isStringOrNumberの戻り値は真偽値だけど、戻り値がtrueだった場合
//somethingにstring|number型が付与される
const isStringOrNumber = (value:unknown):value is string|number => {
    return typeof value === "string" || typeof value === "number";
}
const something:unknown = 232;

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

モジュールシステム

import

現在のファイルからの参照ファイルパスを書く

実際のファイルは拡張子.tsだが、インポートは .js とかく

import {mapOption} from './practice.js'

一括インポート

//sou.js
export const name = "sou";
export const age = 30; 

このようなモジュールは実はimport *構文注11によるインポートが適していま

インポートする側は次のようになります。

index.ts 

import * as uhyo from "./sou.js";

console.log(uhyo.name); // "sou" と表示される
console.log(uhyo.age);  // 30 と表示される

export

書き方1
export const name = "uhyo";
export const age = 26;
export function expoFunc(num:number):string {
    return num.toString();
}

書き方2
const name = "uhyo";
const age = 26;
const expoFunc = (num:number):string => { return num.toString()}
export { name, age , expoFunc  };

※モジュールにしたファイルはカプセル化できる

モジュール化すると、他ファイルは参照しかできない

なので外側から参照した関数を実行すると、import先の変数と連動しており、importされた側の変数も書きかわる

conter.ts

export let value = 0;

export function increment() {
  return ++value;
}

===========================================
index.ts

import { increment, value } from "./counter.js";

increment();
console.log(`カウンタの値は${value}です`); // "カウンタの値は1です" と表示される
increment();
console.log(`カウンタの値は${value}です`); // "カウンタの値は2です" と表示される
increment();
console.log(`カウンタの値は${value}です`); // "カウンタの値は3です" と表示される
 

インポート先で自由に名前を変更できるdefault exportというのもある。

souAge.ts 
export default 26;

index.ts 
import uhyoAge from "./souAge.js";

console.log(`souの年齢は${uhyoAge}です`); // "souの年齢は26です" と表示される

import宣言はエディターが自動で書いてくれるが、defaultだと書いてくれなくなるのであまり使わない方がいいらしい

あとエクスポートするときは「どこのファイルからでてきたか」とうのを変数名に書いてあげる方がいい

たとえば、sou.tsからnameとageをエクスポートするならば、souNameやsouAgeとしたほうがより適切でしょう。

そうすれば、ほかのモジュール(たとえばjohn-smith.ts)がエクスポートする名前と被りにくくなります。

また、インポートする側も、nameだと何の名前かわからないのでsouNameといった別名をつけたくなるかもしれません。

型のimport/export

import

型としてインポートするのか、値としてインポートするのかで書き方が異なる

import type { Animal } from "./animal.js";
import { tama } from "./animal.js";
↕これのようにも書ける、同じ意味
import { tama, type Animal } from "./animal.js";

export

export type Animal = {
  species: string;
  age: number;
}

↕下の書き方だと型と値の出力同時にできる

type Animal = {
  species: string;
  age: number;
};

const tama: Animal = {
  species: "Felis silvestris catus",
  age: 1
};

export { Animal, tama };

※スクリプトとモジュールの違い

exportのあるなしでスクリプトかモジュールかどうか判定される

スクリプトはプロジェクト全体に反映する

スクリプトがモジュールに変更されてコンパイルエラーが発生するというのはあるあるらしい

human.ts 
type Human = {
  name: string;
  age: number;
}; 

index.ts 
export const uhyo: Human = {
  name: "uhyo",
  age: 26
}; 

このようにしてindex.tsをコンパイルすると、無事コンパイルできます。

ここで注目すべき点は、human.tsの中で定義されているHuman型をindex.tsから使用できているということです。
すなわち、スクリプトであるhuman.tsの中のHuman型のスコープがプロジェクト全体に広がっているということです。
次に、human.tsを次のように変更してみましょう。

こうすると、human.tsはexport宣言を含むのでモジュールになります。

human.ts 
export type Human = {
  name: string;
  age: number;
}; 

すると、何ということでしょう。もう一度index.tsをコンパイルすると
Cannot find name ‘Human’.というコンパイルエラーが発生してしまいます

プロジェクト全体で使われる型を定義したい場合は、このようにスクリプトで定義してどこからでも使えるようにする方法と、モジュールで定義して明示的にインポートする方法の2つの選択肢があります。

筆者は後者を推奨しています。

その理由は、前者の場合はスクリプトをモジュールに変更したい(ほかのモジュールからインポートした型をスクリプト内で使いたい)場合に困るからです。

また、必要のない型まですべてスコープに存在しているのは補完の邪魔になりますしバグのもとです。

外部モジュール

外部モジュールは指定ファイルの前に「./」がimport時に必要ない

import fastify from "fastify";

const app = fastify();

app.get('/', (req, reply) => {
    reply.send("Hello, world!");
});

app.listen(8080);

外部モジュールは2種

①組み込みモジュール(node.jsに最初からあるモジュール)

→Node.jsの組み込みモジュールを用いることで、OSのさまざまな機能を利用できます。readlineのほかにも、ファイルシステムへのアクセス(fs)、ネットワーク(net, httpなど)、パスの変換(path)などはよく用いられます。

②npmインストールされたモジュール

→世の中の人が作ってくれた便利モジュールを使える。node_modelesから

@type

型提供をしていないモジュールも存在する

そのときは、有志でつくられた@typeパッケージを利用する

DefinitelyTypedというシステムでマイクロソフトが管理している

大体存在するが、マイナーモジュールだとないときがある

expressの型をインポ-トするときは下記

npm install -D @types/express

npm installの-Dオプションは、インストールしたパッケージをpackage.jsonに書き込む際にdependenciesではなくdevDependenciesのほうに書き込むという意味です。

@typesパッケージはあくまでコンパイル時にのみ必要であるため、devDependenciesに入れるのが通例です。

非同期処理

概念

非同期処理とは裏で動く処理です。そして非同期の典型は時間のかかる処理

  • サーバー同士の通信
  • ファイルの読み書き(CPU⇔HDD・SSD)
  • 他サーバーの処理を待つ(APサーバー⇔DBサーバー)

ブログラムから時間のかかる処理を行うとき APIはブロッキングとノンブロッキングに分かれる

非同期処理はノンブロッキングに属する

ブロッキング処理とは通信している間待ち時間を要する処理

ex:ファイルの読み込みなど

console.log("読み込みを開始します");
const data = readFile("filename.txt");
console.log("読み込みました");

JS・TSはシングルスレッドなので、プログラムの複数箇所が並列に実行されることはない

なので、ブロッキング処理は歓迎されない思想。なぜなら止まってしまうから。

node.jsのWEBサーバー機能を例に挙げます。 

もしブロッキング処理ならクライアント一人の処理にとまっていたら複数アクセスに対応できないです。

非同期処理のコールバック関数

ここでいうコールバック関数は、非同期処理終了後に実行する関数

コールバック関数の実行で、非同期処理の終了を検知できる

import {readFile} from "fs";
import { performance } from "perf_hooks";

//非同期処理
//ファイルを読み込むという非同期処理が終了したら、コールバック関数実行
const startTime = performance.now()
const data = readFile("practice.txt","utf-8",(err,data)=>{
    const endTime = performance.now()
    console.log(`${endTime-startTime}ミリ秒かかりました`)
})
console.log(data)

※非同期処理はちゃんとエラー処理を明示的に行うこと

なぜ?→コールバック関数の引数として渡されるので、適切に処理しないと気づけないから

ランタイムエラー違って、エラーを無視してしまうことが簡単になってしまうから。

コールバック関数として渡す関数はドキュメントや型情報を見て適切に渡す。nodeでは規則がある

ます。Node.jsでは原則としてコールバック関数の引数は2つであり、1つめがエラー・2つめが処理結果となります。

※覚えておくべきこと

Typescriptでは同期的に実行中のプログラムに非同期処理が割り込むことはない

つまり、すべて上から下まで読み込んだ後、コールバック関数は実行される。

非同期処理が完了していたとしても、同期処理が終了するまで実行されない(割り込むことはできない)

//非同期処理に渡した関数は同期処理が終わるまで実行されない
setTimeout(()=>{console.log("タイマーが呼び出されました")},100);

const startTime_2 = performance.now();
let count = 0;
while(performance.now()-startTime_2<1000){
    count ++;
}
console.log(count);

タイマー呼び出しは100ミリ秒に設定しているにもかかわらず、console.logの実行は1000ミリ秒になっていることがわかる

Promiseというオブジェクト

いままでの非同期処理は、コールバック関数を直接引数に設定する、というひとまとまりの処理だった

Promiseは2段階の処理になる

①非同期処理を行った後、処理結果としてPromiseオブジェクトを返す。

②返されたPromiseオブジェクトにコールバック関数を設定し実行する

//promise
//promise版のFSを使う
const p = fs_p.readFile("practice.txt","utf-8")
//非同期処理が終わった後、then以降の関数が実行される
p.then((data)=>{console.log(data)})

promiseオブジェクトに対してthenメソッドにコールバック関数を登録するのが基本的な使い方

非同期処理のエラーはランタイムエラーでもコンパイルエラーも起こらない
プログラマーが適切に処理することが大事

非同期処理が失敗したときの処理の書き方は2つある
※errorはany型とデフォルトでなっている
  ので、unknown型にしておく

①catchを別に入れる方法

p.then((data)=>{console.log(data)})
p.catch((error:unknown)=>{console.log("失敗",error)})

②thenに、成功も失敗もコールバック関数を入れる方法
p.then((data)=>{console.log(data)},
			(error:unknown)=>{console.log("失敗",error)}	
)

コールバックによるAPIに比べると間に余計なオブジェクトが1個挟まっただけのように見えるかもしれませんが、このように「非同期処理そのもの」を表す抽象的なオブジェクトが用意されたことには大きな意味があります。

というのも、前節で見たように、コールバック関数を直接渡す方式ではコールバック関数の形にバリエーションがあるため、使いたいAPIごとにどのようにコールバック関数を渡せばいいか毎回調べる必要があります。

一方で、PromiseベースのAPIでは非同期処理を行う関数ならどんな関数でも「Promiseオブジェクトを返す」という点で共通しており、結果も「Promiseの解決」という共通の機構を通して伝えられます。

これにより、利用する側はPromiseの使い方さえ覚えていれば同期処理の結果を無事に受け取ることができます。

Promiseオブジェクトであれば、帰ってきたPromiseオブジェクト内のdataに対しての処理をコールバック関数として処理すればよい。

なので、直接コールバック関数を使用するAPIより、調べる手間が省けるし、仕様が共通で使いやすい、というメリットがある。

Promiseオブジェクトの作成

Promiseオブジェクトをつくる

非同期処理は自分でPromiseオブジェクトを使って使いやすくできる

promiseはコンストラクタ型引数と1つの引数を持つ

つまりPromise<T>型

引数は(resolve)=>{…}というexecuterと呼ばれる関数になる。

executer関数はコンストラクタ時に即時に実行される

resolveはnew Promiseで内部的に用意される関数
中の非同期処理が終わったらresolveを実行してということで送られてくる
この例でいうとsetTimeoutという非同期処理が終わったらresleveを実行
多くの場合、new PromiseによるPromiseの作成は、コールバック関数ベースの非同期処理をPromiseに変換するために行われます。

このexecuter関数は3秒後にresolveが実行されるような関数
Promiseオブジェクト「createP」は未解決状態で、3秒後に解決される
これによりcreateP.then()で登録された処理が3秒後に実行される
resolveに渡した100はPromiseの結果
3秒後に解決するとき、結果は100になる
つまり、Promise<T>の型引数Tはresolveの実行結果の型になる

const createP= new Promise<number>((resolve) =>{
    setTimeout(() => {resolve(100)},3000)
})
//thenにセットした引数dataは必ず100になる
createP.then((data)=>{console.log("createP:"+data)})

sleepというタイマーをPromiseオブジェクトで作った場合
//タイマーをPromiseでつくる
const sleep = (duration:number) => {
    return new Promise<void>((resolve) => {
        setTimeout(resolve,duration);
    })
}

sleep(3000).then(()=>console.log("3秒"))

//複数の非同期処理を配列にまとめる
//例 複数ファイルの読み込み
const pFoo = fs_p.readFile("foo.txt", "utf8");
const pBar = fs_p.readFile("bar.txt", "utf8");
const pBaz = fs_p.readFile("baz.txt", "utf8");

const p_all = Promise.all([pFoo, pBar, pBaz]);

p.then((results) => {
  console.log("foo.txt:", results[0]);
  console.log("bar.txt:", results[1]);
  console.log("baz.txt:", results[2]);
});

Promiseチェーン

then自体には新たなpromiseオブジェクトが帰ってくる

goodResult.then((data)=>console.log("goodResult"+ data)).then().catch

最後のエラー処理のcatchの処理を必ず行う

async

関数、メソッドの前にasyncキーワードをつけると、たとえその関数内でPromiseが返されていなくても、戻り値の型をPromiseで包んで返します。

await

async関数内で使える式

与えられたPromiseオブジェクトの結果が出るまで待つ

async/await

async functionで必ず戻り値がpromiseが帰ってくる
new Promise の returnではなく、promiseに結果が内包されたオブジェクトが帰ってくる

awaitはpromiseオブジェクトの解決を待つ
sleep(1000)が解決するまで待つ

async function get3():Promise<number>{
    console.log("2:get3が呼び出されました")
    await sleep(1000)
    return 3
}
console.log("1:get3を呼び出しましす")
const p_get3:Promise<number> = get3();
p_get3.then((result)=>{console.log(`4:結果は${result}`)})
console.log("3:get3を呼び出しました")

awaitがあることで、thenのようなPromiseが解決されたら設定されたコールバック関数を実行する、と同様の制御ができる

thenとawaitは、非同期処理が終わった後に実行する、という意味では、同じ役割を持つ。

async/awaitの強力なメリット

const main = async() => {
    //インポート事態に非同期処理が必要なモノはimport()で記述できる
    const {readFile,writeFile} = await import("fs/promises");
    const naiyo = await readFile("practice.txt","utf-8");
    await writeFile("practice.txt",naiyo+naiyo);
    //await writeFileが失敗したときの処理はtry-catheで記述できる
    try {
        console.log("書き込み終了");
    } catch (error) {
        console.log(error);
    }

}

main().then((data)=>{console.log("mainが完了した")})
  • ある非同期処理が終了してから次の非同期処理を実行する、という同期処理的な実装ができること
  • thenを続けるより、記述がわかりやすい
  • await 非同期処理は戻り値をpromiseでなく、非同期処理が解決した値を返す(resolveの解決した値)ため、本来欲しかったデータをstring型などが使える。
  • await 非同期処理 が失敗したときの処理はtry-catchのように記述できるのでわかりやすい (thenやcatchメソッドを使わず、同期処理と同じように書ける)
  • ちなみにawaitが失敗してもasync関数の戻り値はpromiseなので、エラーを内包した形で返却される。

TOPレベルawait

基本的にawaitはasync関数の中でしか使えないが、最近TOPレベルawaitが使用できる

こうすることで非同期処理をPromiseオブジェクトを使わずに使用できるようになるので、簡易的になる

ただしモダンブラウザしか対応してないので非推奨らしい

通常例

async function main() {
  const { readFile, writeFile } = await import("fs/promises");

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

main().then(() => {
  console.log("main()が完了しました");
});

TOPレベルawait

import { readFile, writeFile } from "fs/promises";

const fooContent = await readFile("foo.txt", "utf8");
// 2倍にしてbar.txtに書き込む
await writeFile("bar.txt", fooContent + fooContent);

基本的にawaitがasync内でしか使えない理由は?

Promiseオブジェクトが戻り値に保障されているのはasync関数である。
①まず、ブロッキングをせずに**「ある時間がかかる処理が終了するのを待ってから、セットした処理(コールバック関数)を実行する」という処理を行う挙動**を実現する以上、戻り値をPromiseにする必要がある。Promiseであることでほかの処理が同期的に進行することができる。
②その関数中であれば、たとえawaitの「待つ」という処理を実行しても、ほかのプログラムが処理を待たずに実行できる

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() {
    // (略)
  }
};

Promise.race

タイムアウトの実装

非同期関数の処理が早く終わったほうを採用するという特殊な関数でも実行できる

const data = await Promise.race([
  readFile(dataFile, { encoding: "utf8" }),
  errorAfter1ms()
]).catch(() => {
  return "";
});

例:1ミリ秒待ってファイル読み込みしなければプロセス終了する

import {readFile} from "fs/promises";
import * as path from "path";
import * as url from "url";

//現在のファイルのパスを取得
const nowPath = url.fileURLToPath(import.meta.url);
//ディレクトリのみに加工する
const nowDir = path.dirname(nowPath);
//指定パスの絶対パスを作成
const currentPath = path.join(nowDir,"../practice.txt")

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

const fileRead = async()=>{
    try {
        //タイマースタート
        const data =  await readFile(currentPath,"utf-8")
        //処理が終わってなかったらthorowエラー
        console.log("ファイルを読み込みました")
        return data;
    } catch (error) {
        console.log(error);
        return "error";
    }
};

let startIndex = 0;
let count = 0;
sleep(1).then(()=>{
    console.log("processを修了します")
    process.exit();
})
const fileText = await fileRead();

while(true){
    let nextIndex =  fileText.indexOf("uhyo",startIndex);
    if(nextIndex >= 0){
        count += 1;
        startIndex = nextIndex + 1;
    }else{
        break;
    }
}
console.log(count)

コンパイラオプション

基本はStrictオプション

noImplicitAny

JavaScriptのプログラムをTypeScriptに移行する場合

noUncheckedIndexedAccess

インデックスアクセスを厳しくする

通常だと存在しないインデックスにアクセスすると「number」となり、中身が入っていないのにnumberとなってしまう。それを伏防ぐ機能

// arrはnumber[]型
const arr = [1, 2, 3];

console.log(arr[0]); // 1 と表示される
console.log(arr[10]); // undefined と表示される

おすすめ機能

・strictオプションは有効にする(デフォルトのtsconfig.jsonですでに有効)。

・さらに、noUncheckedIndexedAccessも有効にする。

・exactOptionalPropertyTypes(6.1.5)も有効にする。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次