TypeScriptを始めよう:Advanced Types

前回までの記事に沿って説明をしているので是非、今までの記事を確認お願いします。

では前回作成したTypeScriptのオブジェクトを見てみましょう。このコードに何が問題があるでしょうか?

let user: {
  readonly id: number;
  name: string;
  promoted: (date: Date) => void;
} = {
  id: 1,
  name: "",
  promoted: (date: Date) => {
    console.log(date);
  },
};

問題①次のuserオブジェクトを作成したい際に、同じようにデータタイプを宣言する必要がある。

問題②他のuserには異なるプロパティ(キー)が存在する可能性がある。

問題③コードが読みづらい。

TypeScript: type alias

下記の様にTypeScriptのtypeを使ってクラスのようにデータタイプを宣言できます。

type User = {
  readonly id: number;
  name: string;
  promoted: (date: Date) => void;
};

let user1: User = {
  id: 1,
  name: "Yoshi",
  promoted: (date: Date) => {
    console.log(date);
  },
};

let user2: User = {
  id: 2,
  name: "Mike",
  promoted: (date: Date) => {
    console.log(date);
  },
};

TypeScript:Union Types

ライブラリなどで引数のデータタイプがStringやNumberなどに指定されている場合があります。このようにTypeScriptではデータタイプによってどのようなコードを実行したいか指定することができます。

では摂氏から華氏に気温を変換する関数の例を見てみます。

関数のパラメータがStringでもNumberでも実行できるようになっています。

function CtoF(temp: number | string):number {
  if (typeof temp === 'number') 
    return temp * 9/5 + 32
   else 
    return parseInt(temp) * 9/5 + 32
}

CtoF(30); 
CtoF('20度');

ではtscのコマンドで.jsファイルにコンパイルするとこのようになりました。

"use strict";
function CtoF(temp) {
    if (typeof temp === 'number')
        return temp * 9 / 5 + 32;
    else
        return parseInt(temp) * 9 / 5 + 32;
}
CtoF(30);
CtoF('20度');
//# sourceMappingURL=index.js.map

TypeScript: Intersection Types

では、最初に作成したユーザーのモデルの例で、ユーザーに特定のプロパティを追加するにはどうすればよいでしょうか。TypeScriptではIntersection Types:インターセクション型というオブジェクトの定義を組み合わせる方法があります。

type User = {
  readonly id: number;
  name: string;
  promoted: (date: Date) => void;
};

type Admin = {
  password: string;
};

type SuperUser = User & Admin

let manager1: SuperUser = {
  id: 2,
  name: "Mike",
  promoted: (date: Date) => {
    console.log(date);
  },
  password: "abc"
};

これでUserオブジェクトとSuperUserオブジェクトの重なるコードを繰り返す必要がなくなりました。

TypeScript リテラル型 (literal type)

JavaScriptで受け入れられるデータは大体予測できる場合がありますね。TypeScriptのリテラル型 (literal type)を使用することで、変数のデータを前もって制限することができます。

このようにnum1の変数に10か20の数字が入ることを指示した場合はnum1に12を割り当てようとするとエラーが出ます。

let num1: 10 | 20 = 10;
num1 = 12; //Type '12' is not assignable to type '10 | 20'.ts(2322)

また、前回で使用したtypeを使って別に記載することもできますね。

type Num = 10 | 20

let num1:Num  = 10;

TypeScript Nullable Type

JavaScriptのundefinedやnullによって発生するバグは多いです。

例のようにgreet関数を実行するとエラーが発生します。

function greet(name) {
  console.log(name.toUpperCase());
}
greet();
//TypeError: Cannot read properties of undefined (reading 'toUpperCase')

これは、greet関数にあるtoUpperCase()メソッドを実行する際にundefined.toUpperCase()のようになってしまったからです。

同じことをTypeScriptで行おうとするとエラーが出ます。

function greet(name:string) {
  console.log(name.toUpperCase());
}
greet(undefined);
//Argument of type 'undefined' is not assignable to parameter of type 'string'.ts(2345)

greet(null);
//Argument of type 'null' is not assignable to parameter of type 'string'.ts(2345)

これはTypeScript側でundefinedとnullの使用に制限がかかっているからです。

ではtsconfig.jsonファイルのコンフィグを見てみましょう。

"strictNullChecks": false,                         /* When type checking, take into account 'null' and 'undefined'. */

このstrictNullChecksのコメントアウトを外してfalseに設定すると

greet(null);

greet(undefined);

の両方を呼んでもエラーが発生しなくなりました。

しかし、実際の開発の際にこのオプションを使う事はないと思うので知っておく程度に理解しておきましょう。

nullやundefinedを受け入れる関数を作成したい場合はこのように書くようにしましょう。

function greet(name:string | null | undefined) {
  if (name)
    console.log(name.toUpperCase());
  else 
    console.log("ようこそ");

}
greet(null);
greet(undefined);

これでnullとundefinedの場合にエラーが発生することがなくなりましたね。

TypeScript オプショナルチェーン  (?.)

TypeScriptのオプションチェーンはJavaScriptでも導入されたオペレーターになります。

では次のコードを見て何が問題になるか考えてみてください。

type Customer = {
  birthday: Date;
};

function getCustomer(id: number): Customer | null {
  return id === 0 ? null : { birthday: new Date() };
}

let customer = getCustomer(0);

console.log(customer.birthday)

//let customer: Customer | null
//'customer' is possibly 'null'.ts(18047)

ここで最後のconsole内のcustomer.birthdayの部分でエラーが発生しました。

内容はcusomterがnullの可能性があります。というものです。もしnullの場合はnull.birthdayのコードは成立しないので問題になってしまいますね。

これを解決するにはこのように書くことができますね。

if (customer !== null)
console.log(customer.birthday)

しかし、undefinedの引数が入る可能性もあるのでこのように書くことになります。

type Customer = {
  birthday: Date;
};

function getCustomer(id: number): Customer | null | undefined {
  return id === 0 ? null : { birthday: new Date() };
}

let customer = getCustomer(0);

if (customer !== null && customer !== undefined)
console.log(customer.birthday)

しかし!TypeScript(JavaScriptでもありますが、)でシンプルに解決する方法があります。それがオプショナルチェーン  (?.)です。

このようにプロパティの後に?を付けることができます。

// if (customer !== null && customer !== undefined)
console.log(customer?.birthday)

では、このコードを実行してみましょう。

tsc

node dist/index.js

#結果
undefined

このように定義されていない.birthdayはundefiendで返ってきました。

これを0以外を入れるとちゃんとDateが返ってきました。

let customer = getCustomer(1);

console.log(customer?.birthday)
//2023-03-23T19:35:39.770Z

さらに応用編としてtype内のキーをオプショナル?にして、birthdayがあったら、どうするか定義してみます。

type Customer = {
  birthday?: Date;
};

function getCustomer(id: number): Customer | null | undefined {
  return id === 0 ? null : { birthday: new Date() };
}

let customer = getCustomer(1);

console.log(customer?.birthday?.getFullYear())

このようにcustomer?.birthday?.getFullYear()ではcustomerがあったらbirthdayを見る、そしてbirthdayがあったらgetFullYear()を実行するようにできます。

おまけ

上記で説明したオプショナルチェーンのリリースです。

  • TypeScriptでのリリース日:2019年11月 バージョン 3.7
  • JavaScriptでのリリース日:2020年7月 ECMAScript 2020 (ES2020)

これで分かるようにTypeScriptで導入された良い機能はJavaScriptでも使用される可能性があります。

TypeScriptは今後も必須のツールになりそうですね。