TypeScript 応用的な型

前回に引き続きTypeScriptを学ぼうシリーズ。

前回まではこちら

https://www.sunapro.com/typescript%e3%81%ae%e5%9e%8b%e5%9f%ba%e6%9c%ac/

https://www.sunapro.com/typescript-%e3%82%a4%e3%83%b3%e3%82%bf%e3%83%bc%e3%83%95%e3%82%a7%e3%83%bc%e3%82%b9/

今回は応用的な型について色々まとめる。

目次

交差型

オブジェクト型を結合すると論理和になる。

Union型を結合すると共通部分の論理積になる。

type Admin = {
  name: string;
  privileges: string[];
}

type Employee = {
  name: string;
  startDate: Date;
}

type ElevatedEmployee = Admin & Employee; // name, privileges, startDateを持つ


type Combinable = string | number;
type Numeric = number | boolean;

type Universal = Combinable & Numeric; // numberになる

タイプガード

引数に複数の型を持つ引数を取る場合に、その型に応じた処理を記述すること。

例えば受け取った引数がstringならAの処理、numberならBの処理という感じ
これにより意図しない挙動を防ぐことができる。

function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString(); // 文字列の結合
  }
  return a + b; // 数字の足し算
}

以下のように型のUnion型の場合はtypeofは使えないのでinを使う

type UnknownEmployee = Employee | Admin;


// privilegesはAdminしか持っていない
function printEmployeeInformation(emp: UnknownEmployee) {
  if ('privileges' in emp) { // ここでtypeofは使えない
    console.log(emp.privileges) // 受け取った引数がAdminだった場合のみ実行される
  }
  ...
}

上記のやり方だとprivilegesのタイポの可能性があるので別の方法にしたい。
要するに引数として受け取ったものがどこからきた物なのかを関数の中で判別できればいい。

もしUnion型で指定したものがclassだった場合(EmployeeやAdminがclassの場合)は、次のようにinstanceofを使って記述することが可能。

function printEmployeeInformation(emp: UnknownEmployee) {
  if (emp instanceof Admin) {
    console.log(emp.privileges)
  }
  ...
}

ただしこれはインターフェースの場合は使えない。
なぜならinstanceofはJSのメソッドで、どのインスタンスかを判別するのに使うことができるが、インターフェースはJSにコンパイルされないため。

インターフェースの場合は次のDescriminated Union型を使う

判別可能な(Descriminated)Union型

受け取る引数がインターフェースの場合に、そのインターフェースが何かを知るのはDescriminated Union型を使う。

これはそれぞれのインターフェースに共通のプロパティを用意する方法である。
以下のケースではtype。

interface Bird {
  type: 'bird';
  flyingSpeed: number;
}

interface Horse {
  type: 'horse';
  runningSpeed: number;
}

type Animal = Bird | Horse;

function moveAnimal(animal: Animal) {
  let speed;
  switch (animal.type) {
    case 'bird':
      speed = animal.flyingSpeed;
      break;
    case 'horse':
      speed = animal.runningSpeed;
      break;
  }
  console.log(speed)
}

関数内でこのtypeを判別することで、引数がどのインターフェースかを判断することができる。

これによってmoveAnimal(Bird)とした時とmoveAnimal(Horse)とした時でそれぞれのspeedに関するプロパティが異なったとしても、その差は関数内のswitchで吸収してくれている。

関数を呼び出す際にそのインターフェースが何のプロパティを持っているかは気にする必要がない。

オブジェクトやUnion型を扱う際に非常に便利。

型キャスト

TypeScriptでは基本的には型推論に任せていいっぽい。
推論した型が違う場合に明示的に型を示すものが型キャスト。

<input type="text" id="user-input"></input>
const userInputElement = document.getElementById("user-input")!;
// userInputElementはHTMLElementとなる
// 最後に!があることによってnullを許容する

userInputElement.value = 'hello'; // エラー

ここでuserInputElementはHTMLElementである。

valueのような特定のElement(ここではinput)固有のプロパティは使えない。
したがって、ここではTypeScriptにこれがinputタグであることを教えてあげる必要がある。

以下の2つのやり方がある。

const userInputElement = <HTMLInputElement>document.getElementById("user-input")!;

const userInputElement = document.getElementById("user-input")! as HTMLInputElement;

型キャストを使う場合は、その要素が持つべき型の保証はプログラマーが行わなければならない。

インデックス型

あらかじめどのプロパティを持つかを定義できないときは、インデックス型を使う。

interface ErrorContainer {
  [prop: string]: string;
}

const errorBag: ErrorContainer = {
  email: '正しいメールアドレスではありません',
  username: 'ユーザー名に記号を含めることはできません'
};

上記の場合、interfaceでは何のプロパティを持つかを明確に定義しているわけではないので、たとえばemailのプロパティは削除しても良い。

ErrorContainerをメールアドレスなどに限定したくない場合に使うことができる。

prop: stringのstringはプロパティ名がstringであることを、その後のstringはvalueがstringであることを規定している。

関数オーバーロード

返り値の型を正しく推論できない場合に使う。

type Combinable = string | number;

function add(a: Combinable, b: Combinable) => {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

const result = add('hello', 'typescript'); //resultはCombinableと認識される
result.split(' '); // splitはstringに対して使うメソッドなのでエラー

この場合、resultの返り値の推論はCombinableとなっているため、result.splitはエラーとなる。

このような時に、a, bともにstringの場合はstringの返り値、ともにnumberの場合はnumberの返り値、というように指定することができるのが関数オーバーロード。

type Combinable = string | number;

function add(a: number, b: number): number; // a,bともにnumberの場合は返り値もnumber
function add(a: string, b: string): stirng; // a,bともにstringの場合は返り値もstring
function add(a: Combinable, b: Combinable) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
  return a + b;
}

const result = add('hello', 'typescript'); //resultはstringと認識される
result.split(' ');

オプショナルチェイン

オブジェクトをバックエンドにリクエストする場合などは、目的のオブジェクトが何らかの理由で得られない場合がある。

例えば以下のようなオブジェクトをバックエンドから取得する時にjobオブジェクトを得られない場合、エラーが起こりうる。

const fetchedUserData = {
  name: 'user1',
  job: {
    title: 'Developer',
    dscription: 'Typescript'
  }
}

console.log(fetchedUserData.job.title); //jobがundefinedの場合エラー

このような場合に、オプショナルチェインを用いる

console.log(fetchedUserData?.job?.title);

?があることでundefinedであればその後のメソッドを実行しないためエラーにはならずに済む。

null合体演算子

値がundefinedやnullだった場合にデフォルト値を使いたい場合、下記のように書くことができる。

const data = userInput ?? 'Default';

これはuserInputがundefinedもしくはnullだった場合にはdataが’Default’になることを意味する。

しかし、userInputが空の文字列””だった場合にはそのまま空の文字列が適用される。

もし空の文字列のようなfalsyな値も考慮したい場合には以下のように書く。

const data = userInput || 'Default';

これはuserInputが空の文字列であってもDefaultとなる

おわりに

今回は色んな応用的な使い方について見てきた。
すぐには全部は使いこなせない気がするけど、まとめられたので徐々に慣れていこう。

このシリーズも長くなってきた。。

最後にジェネリクスについてまとめて一旦終了。

関連記事

TypeScriptの型基本についてはこちら

https://www.sunapro.com/typescript%e3%81%ae%e5%9e%8b%e5%9f%ba%e6%9c%ac/

インターフェースについてはこちら

https://www.sunapro.com/typescript-%e3%82%a4%e3%83%b3%e3%82%bf%e3%83%bc%e3%83%95%e3%82%a7%e3%83%bc%e3%82%b9/

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