LoginSignup
87
78

More than 1 year has passed since last update.

サバイバルTypeScriptを読むまで知らなかったこと

Last updated at Posted at 2022-10-16

TypeScriptは業務で使っていますが、このサイトで改めて勉強し、初めて知ったことが結構あったので、アウトプットがてら記事に残すことにしました。
まだサイトの序盤を読んでいる段階なので、適宜追記していきます。
(2022/12/16 読了しました)

それぞれのh2タイトルはサバイバルTypeScriptの該当ページへのリンクになっています。
また実際に自分で試したコードを載せていますが、参照元とほぼ同じだったりします。ごめんなさい。

JSONにおいてのundefinedとnullの違い

JSON.stringifyでJSON化した時、値がundefinedのプロパティは削除される。

const obj = { name: null, age: undefined };

// {"name":null}が出力される
console.log(JSON.stringify(obj));

配列が含まれる場合、その配列内のundefinedはnullに変換される。

const obj = { name: [null, undefined] };

// {"name": [null, null]}が出力される
console.log(JSON.stringify(obj));

JSONをオブジェクトにパースする際も、undefinedは認識されず実行時にエラーとなる。

const str: string = '{"name":null, "age":undefined}';

// ランタイムエラー:SyntaxError: Unexpected token u in JSON at position 20
console.log(JSON.parse(str));

readonlyとconstの違い

readonlyはプロパティへの代入を禁止するが、変数自体への代入は許可する。

let obj: { readonly x: number } = { x: 1 };

// コンパイルエラー:Cannot assign to 'x' because it is a read-only property.
obj.x = 10;

// OK
obj = { x: 100 };

オプショナルプロパティへの代入時の挙動

undefinedは代入できるが、nullはできない。

let obj: { x: number; y?: number };

// OK
obj = { x: 10, y: undefined };

// コンパイルエラー:Type 'null' is not assignable to type 'number | undefined'
obj = { x: 10, y: null };

余剰プロパティチェックの挙動

余剰プロパティチェックはオブジェクトリテラルの代入に対してのみ行われる。
変数代入には行われない。

// コンパイルエラー:Type '{ x: number; y: number; }' is not assignable to type '{ x: number; }'.
const x: { x: number } = { x: 1, y: 2 };

const xy: { x: number; y: number } = { x: 1, y: 2 };
// OK
const x2: { x: number } = xy;

「object」型指定を避けるべき理由

1.何のプロパティを持っているかの情報が無いため、プロパティを参照しようとするとエラーになる。

let obj: object = { x: 1, y: 2 };

// コンパイルエラー:Property 'x' does not exist on type 'object'.
console.log(obj.x);

2.オブジェクト型であれば何でも代入できてしまう。

let obj: object = { x: 1, y: 2 };

// 全てOK
obj = { name: "taro", age: 30 };
obj = [1, 2, 3];
obj = () => {};
obj = /^$/;

オプショナルチェーン

配列の参照や関数の呼び出しにも使えるとは知りませんでした。

const arr = null;

// コンパイルエラー:Object is possibly 'null'.
console.log(arr[100]);

// OK
console.log(arr?.[100]);
const func = null;

// コンパイルエラー:Cannot invoke an object which is possibly 'null'.
console.log(func());

// OK
console.log(func?.());

TypeScriptは構造的部分型を採用している

ある型に別の型を代入する際、継承の関係を必要とする公称型と異なり、TypeScriptでは構造が同一であれば代入可能。
つまり全く関係のないクラスでもプロパティやその型、メソッドやその戻り値等が同一であれば代入可能。

注意点として、publicでないプロパティやメソッドはその部分だけ公称型となり、代入の可否が変わる。

class Human {
  constructor(public name: string) {}
}

class Person {
  constructor(public name: string) {}
}

class President extends Human {
  // 例えばここをprotected or privateにすると公称型となり、Employee型とPresident型を相互に代入できなくなる
  public age: number;

  introduce() {
    console.log(`Hi, I'm ${this.name}, a president.`);
  }
}

class Employee extends Person {
  public age: number;

  introduce() {
    console.log(`Hi, I'm ${this.name}, an employee.`);
  }
}

// 継承元も異なり、相互に関係の無い両クラスだが、構造が同じなのでOK
const employee: Employee = new President("taro");

// false
console.log(employee instanceof Employee);
// true
console.log(employee instanceof President);

interfaceは実装しなければ使用できないPHP等と異なり、interfaceを実装していないオブジェクトの型注釈として使用できるのは構造的部分型の恩恵と言える。

interface Person {
  name: string;
  age: number;
}

// 実装していないが、問題なく使用可能
const User: Person = {
  name: "taro",
  age: 30,
};

配列要素へのアクセス

存在しない配列要素へのアクセスはコンパイル時にエラーとならず、またundefined型の可能性を考慮しない。
そのため以下のようにundefinedが紛れ込み、ランタイムエラーが起こりうる。

const arr: string[] = ["apple", "banana"];

// 実際はundefinedが入るが、string型と推論される
const selected = arr[10];

// undefined
console.log(selected);

// コンパイルを通っているが、undefinedにtoUppserCaseメソッドは無いので当然エラー
// ランタイムエラー:Cannot read properties of undefined
console.log(selected.toUpperCase());

対処法:
コンパイラーオプションのnoUncheckedIndexedAccessを有効にする。
すると上記のケースはstring | undefinedと推論され、undefinedのチェックが必要になる。

const arr: string[] = ['apple', 'banana'];

// string | undefined のユニオン型と推論される
const selected = arr[10];

// コンパイルエラー:Object is possibly 'undefined'
console.log(selected.toUpperCase());

// undefinedチェックが必須になるので、安全
if (typeof selected === 'string') {
    console.log(selected.toUpperCase());
}

分割代入も同様。

const arr: string[] = ['apple', 'banana'];
const [a, b, c] = arr;

// noUncheckedIndexedAccessが無効の場合:stringと推論されるのでコンパイルを通る
console.log(c.length);

// noUncheckedIndexedAccessが有効の場合:stirng | undefinedのユニオン型と推論されるので、undefinedチェックが必要
// コンパイルエラー:Object is possibly 'undefined'
console.log(c.length);

読み取り専用配列の挙動

1.readonly arrayには、破壊的変更をするメソッドが使えない

const fruits: readonly string[] = ["apple", "banana"];

// 以下はいずれもコンパイルエラー Property 〇〇 does not exist on type 'readonly string[]'.
fruits.push("cherry");
fruits.sort();
fruits.reverse();

2.readonly配列は通常配列に代入できないが、逆は可能。

// readonly配列は通常配列へ代入できない
const fruits: readonly string[] = ["apple", "banana"];
// コンパイルエラー:The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 
const normalArr: string[] = fruits;

// 逆に、通常配列はreadonly配列へ代入できる
const animals: string[] = ["dog", "cat"];
const readonlyArr: readonly string[] = animals;

あれ?
通常配列をreadonly配列に代入できるということは、代入元の通常配列を操作すると、、?

const animals: string[] = ["dog", "cat"];
const readonlyArr: readonly string[] = animals;

// 代入元をいじってみた
animals.push("bird");
animals.sort();

// ['bird', 'cat', 'dog']
console.log(readonlyArr);

readonly配列も書き変わってしまいますね、これは気をつけよう。

配列の共変性で気をつけること

共変とは
ある型とその部分型が代入できること。
以下の例では、DogはAnimalの部分型であるため、Animal型の変数には、Animal型とDog型を代入できる。

共変性を利用して、こんなことが起こり得ます。
※詳細は長くなるため、サバイバルTypeScriptのページを直接確認いただいた方が良いかと思います。

// サバイバルTypeScriptではinterfaceを使用した方法が紹介されているため、私はclassで書いてみました。
class Animal {
  constructor(public isAnimal: boolean) {}
}

class Dog extends Animal {
  constructor(public isAnimal: boolean, public isDog: boolean) {
    super(isAnimal);
  }

  wanwan() {
    return "wanwan"; // メソッドの実装
  }
}

// Animalクラスを継承したDogクラスのインスタンスを生成
const pochi: Dog = new Dog(true, true);

// Dogクラスの配列に入れる
const dogs: Dog[] = [pochi];

// DogはAnimalの部分型なので、Animalクラスの配列にも代入可能
const animals: Animal[] = dogs;

// 配列はオブジェクトなので、dogs[0]も書き換わる
animals[0] = new Animal(true);
// Animal { isAnimal: true }
console.log(dogs[0]);

// dogs[0] は既にAnimalクラスのインスタンスに差し代わっているため、Dogクラスのメソッドを呼び出すとエラーになるが、コンパイル時にはスルーされるため実行時に気づくこととなる
const mayBePochi: Dog = dogs[0];
// ランタイムエラー:mayBePochi.wanwan is not a function
mayBePochi.wanwan();

ユニオン型の型注釈の書き方

改行してこんな風に書ける。

type ErrorCode =
  | 400
  | 401
  | 402
  | 403
  | 404
  | 405;

最初のパイプが必須になるわけでは無い。

type ErrorCode =
  400
  | 401
  | 402
  | 403
  | 404
  | 405;

コンパイルエラーになる型アサーション

型アサーションを使用すれば型情報を強制的に上書きできるが、無理のあるものはコンパイルエラーになる。

const num: number = 1;
// コンパイルエラー:Conversion of type 'number' to type 'boolean' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
const bool: boolean = num as boolean;

エラーに言われた通り、unknownを経由することで型の書き換えが可能。
anyやneverでも出来ました。

const num: number = 1;
// 全てコンパイルOK
const bool1: boolean = num as unknown as boolean;
const bool2: boolean = num as any as boolean;
const bool3: boolean = num as never as boolean;

あくまでも型情報の書き換え(キャストではない)ため、実行時のタイプは変わらない点に注意です。

const num: number = 1;
const bool: boolean = num as unknown as boolean;
// number
console.log(typeof bool);

readonlyとconst assertionの違い

readonlyをつけたプロパティがオブジェクトの場合、そのプロパティや要素まではreadonlyにしない。

type Engineer = {
  readonly name: {
    first: string;
    last: string;
  };
  readonly skills: string[];
};

const e1: Engineer = {
  name: {
    first: "taro",
    last: "test",
  },
  skills: ["typescript", "go"],
};

// readonlyプロパティ自体なので、以下2行はコンパイルエラー
e1.name = { first: "", last: "" };
e1.skills = [];

// readonlyプロパティが持つプロパティ・要素への変更はコンパイルエラーとならない
e1.name.first = "jiro";
e1.skills[0] = "javascript";
e1.skills.push("php");

// jiro
console.log(e1.name.first);
// [ 'javascript', 'go', 'php' ]
console.log(e1.skills);

as constを宣言すると、

  • 配下のネストしたオブジェクトプロパティ全てにreadonly属性がつく(再帰的になる)
  • 配列はreadonly配列となる
const e1 = {
  name: {
    first: "taro",
    last: "test",
  },
  skills: ["typescript", "go"],
} as const;

// 以下全てコンパイルエラー
// Cannot assign to 'first' because it is a read-only property.
e1.name.first = "jiro";
// Cannot assign to '0' because it is a read-only property.
e1.skills[0] = "javascript";
// Property 'push' does not exist on type 'readonly ["typescript", "go"]'.
e1.skills.push("php");

!アサーションの2種類の使い方

本質的には2つとも同じなのかも知れませんが、普段使う機会がなくよく理解していませんでした。
2パターンいずれも、コンパイラオプション「strictNullChecks」がtrueで無いとエラーは出ません。

1. definite assignment assertion
変数の初期化が別の関数内でされているような場合、検知されずコンパイルエラーになる。

let num: number;

const init = () => {
  num = 100;
};

init();
// コンパイルエラー:Variable 'num' is used before being assigned.
console.log(num);

宣言時に!をつけることで、エラーを回避可能。

let num!: number;

const init = () => {
  num = 100;
};

init();
// 宣言時では無くここでnum!とすることにより、後述の非Nullアサーションとしてエラー回避も可能
console.log(num);

2. 非Nullアサーション
コンパイラにnullでは無いことを伝える。

// HTMLInputElement | null と推論される
const inputDom = document.querySelector("input");
// コンパイルエラー:Object is possibly 'null'.
console.log(inputDom.value);
// OK
console.log(inputDom!.value);

等価演算子が想像以上に緩い

// 全てtrue
console.log(0 == false);
console.log(0 == "");
console.log(0 == "0");
console.log(false == "0");
console.log(false == "");
console.log(null == undefined);

// これだけfalse
console.log("0" == "");

ちなみに、NaNは常にfalse

const notANumber = NaN;
// false
console.log(notANumber == notANumber);

switchは厳密等価演算

元々PHPを使用していたので、JSも同様に緩い等価演算だと思っていました。。

function test(n: unknown) {
  switch (n) {
    case 1:
      return "This is number";
    case "1":
      return "This is string";
    default:
      return "other";
  }
}

// This is string
console.log(test("1"));

switchにおける変数スコープ

case・defaultは変数スコープを生成しないため、変数名が被るとコンパイルエラーとなる。

switch (true) {
  case true:
    // コンパイルエラー:Cannot redeclare block-scoped variable 'code'.
    const code = 0;
    break;
  defalut: 
    // コンパイルエラー:Cannot redeclare block-scoped variable 'code'.
    const code = 1;
    break;
}

case・defaultに中カッコを書けるため、スコープを分けることも出来る。

switch (true) {
  case true: {
    const code = 0;
    break;
  }
  defalut: {
    const code = 1;
    break;
  }
}

never型の値はどんな型へも代入可能

(never型を除き)いかなる型の値もnever型へは代入できないが、逆は可能。

const none = 1 as never;

// いずれもOK
const str: string = none;
const num: number = none;

isを使用した関数の型ガード

ユニオン型を絞り込むとき、intypeofinstanceoftype guard等を使用するかと思います。

type Dog = {
  bark(): void;
};

type Bird = {
  fly(): void;
};

function doSomething(animal: Dog | Bird) {
  if ("fly" in animal) {
    animal.fly();
  }
}

型の絞り込み部分を関数に切り出した時、その関数で絞り込んだ型は呼び出し元では共有されず、エラーとなる。

function isBird(animal: Dog | Bird): boolean {
  // DogかBirdのいずれかに絞り込まれる
  return "fly" in animal;
}

function doSomething(animal: Dog | Bird) {
  if (isBird(animal)) {
    // Bird型の場合のみ条件分岐を通るが、型推論はユニオンのまま
    // コンパイルエラー:Property 'fly' does not exist on type 'Dog | Bird'.
    animal.fly();
  }
}

絞り込み関数のreturnの型にisを使用することで、呼び出し元でも型の絞り込みが保持される。

function isBird(animal: Dog | Bird): animal is Bird {
  return "fly" in animal;
}

// OK
function doSomething(animal: Dog | Bird) {
  if (isBird(animal)) {
    // Bird型に推論される
    animal.fly();
  }
}

typeofでも同様。

// function isString(val: string | number): boolean {
function isString(val: string | number): val is string {
  return typeof val === "string";
}

function log(val: string | number) {
  if (isString(val)) {
    // isStringのreturnの型付けが単にbooleanだとコンパイルエラー
    console.log(val.length);
  }
}

instanceofでも同様。

class Dog {
  bark() {}
}
class Bird {
  fly() {}
}

// function isBird(animal: Dog | Bird): boolean {
function isBird(animal: Dog | Bird): animal is Bird {
  return animal instanceof Bird;
}

function doSomething(animal: Dog | Bird) {
  if (isBird(animal)) {
    // isBirdのreturnの型付けが単にbooleanだとコンパイルエラー
    animal.fly();
  }
}

asserts + 例外を使用することで型の絞り込みをする方法もある。

// 戻り値の型指定にassertsを追加
function isBird(animal: Dog | Bird): asserts animal is Bird {
  if (animal instanceof Bird) return;

  throw new Error("Wrong Instance!");
}

function doSomething(animal: Dog | Bird) {
  // この時点ではanimalはBird型か分からないので、コンパイルエラー
  animal.fly();

  // Birdでなければ例外が投げられるので、以降animalはBird型に絞られる
  isBird(animal);

  // animalはBird型なのでエラーは出ない
  animal.fly();
}

関数式で関数名をつけた際の挙動

以下のように、関数式addにつけた関数名incrementは、その関数内でのみ呼び出せる。
以下例のように関数を再帰的に呼び出す際に使われるとのこと。

const add = function increment(n: number) {
  if (n < 10) {
    return increment(++n);
  }

  return;
};

add(1);

// コンパイルエラー:Cannot find name 'increment'.
increment(1);

とはいえ、わざわざ名前をつけなくても同じ処理は可能。

const add = function (n: number) {
  if (n < 10) {
    return add(++n);
  }

  return;
};

add(1);

関数から関数の型を宣言できる

定義された関数に対してtypeofを使用することで、関数の型を取得・宣言可能。

function increment(num: number): number {
  return num + 1;
}
// type Increment = (num: number) => number
type Increment = typeof increment;

const decrement = (num: number): number => num - 1;
// type Decrement = (num: number) => number
type Decrement = typeof decrement;

従来の関数とアロー関数の違い

アロー関数の違いは以下2点くらいしか知りませんでしたが、他にも色々ありました。

  • コンストラクタ関数になれない
  • 自身のthisを持たない(アロー関数が定義されているスコープに基づいてthisを定義する)

アロー関数はレキシカルスコープ(静的スコープ)である

先に従来の関数の挙動から確認すると、thisは動的スコープのため、実行時に決定される。
そのため以下の例ではuser.nameが出力される。

window.name = "taro";

function sayName() {
  // thisは実行時のスコープ(user オブジェクト)を指す
  console.log(this.name);
}

const user = {
  name: "jiro",
  sayName,
};

// jiro
user.sayName();

一方アロー関数のthisは定義時に決定されるため、window.nameが出力される。

window.name = "taro";

const sayName = () => {
  // thisは定義時のスコープ(window オブジェクト)
  console.log(this.name);
};

const user = {
  name: "jiro",
  sayName,
};

// taro
user.sayName();

※TypeScriptではコンパイルエラーとなるため、上記のコードはJavaScript環境で実行しています。

call, apply, bindの第一引数は無視される(thisを上書きしない)

従来の関数

function sample() {
  console.log(this);
}
// undefined
sample();
// "test"
sample.bind("test")();

アロー関数

const arrowSample = () => {
  console.log(this);
};
// undefined
arrowSample();
// undefined
arrowSample.bind("test")();

※sloppy modeでは、undefinedの箇所はwindownオブジェクトになります。

arguments変数が存在しない

const func = (...args: number[]) => {
  // コンパイルエラー:Cannot find name 'arguments'
  console.log(arguments);
};
func(1, 2, 3);

ジェネレーターが使えない

そもそも、JavaScriptにジェネレーター関数があること自体知りませんでした。。
↓ 従来の関数で書いたジェネレーター

// ここの*がジェネレーター関数の目印
function* genCounter() {
  let i = 0;
  while (true){
    yield i++;
  }
}

const gen = genCounter()
// { "value": 0, "done": false } 
console.log(gen.next());
// 1
console.log(gen.next().value);
// 2
console.log(gen.next().value);

undefined型とvoid型の違い

「戻り値のない関数」の戻り値としてvoid型もしくはundefined型を指定できるが、undefined型の場合はreturnを省略できない。
※戻り値がない場合、実際にはundefinedが返るという前提がある。

void型の場合、以下のいずれもOK。

function say1(): void{
    console.log("hello");
}

function say2(): void{
    console.log("hello");

    return;
}

function say3(): void{
    console.log("hello");

    return undefined;
}

undefined型の場合、returnを省略するとエラー。

// コンパイルエラー:A function whose declared type is neither 'void' nor 'any' must return a value.
function say1(): undefined{
    console.log("hello");
}

// 以下はいずれもOK。
function say2(): undefined{
    console.log("hello");

    return;
}

function say3(): undefined{
    console.log("hello");

    return undefined;
}

デフォルト引数に明示的にundefinedを渡した場合、デフォルト値が使用される

省略された引数はundefinedになるJSの仕様からすると、当然の挙動かも。

function sayHi(name: string = "taro") {
    console.log(`Hi, ${name}!`);
}

// いずれも "Hi, taro!" が出力される。
sayHi();
sayHi(undefined);

nullの場合はデフォルト値にならない。

function sayHi(name: string | null = "taro") {
    console.log(`Hi, ${name}!`);
}
// 出力:"Hi, null!"
sayHi(null);

オプション引数と似ているが、オプション引数はundefinedとのユニオン型になる点が異なる。

// これはOK
function defaultSayHi(name: string = "ryota") {
    // nameはstring型
    console.log(`Hi, ${name.toUpperCase()}!`);
}

// コンパイルエラー:Object is possibly 'undefined'.
function optionSayHi(name?: string) {
    // nameに対してundefinedチェックが必要
    console.log(`Hi, ${name.toUpperCase()}!`);
}

関数とクラスメソッドの第1引数にthis引数を使用可能(アロー関数を除く)

アロー関数を除いてthisは実行時に決定されるため、以下のようなコードはコンパイル時にエラーと認識されず、実行時にエラーとなる。

class User {
  name: string = "taro";

  sayName(this: User) {
    console.log(this.name);
  }
}

const u1 = new User();
// これは"taro"と正しく出力されるのでOK
u1.sayName();

// コピーされた関数内では、thisはUserクラスを指さない
const copyFunc:() => void = u1.sayName;
// ランタイムエラー:Cannot read properties of undefined (reading 'name') 
copyFunc();

上記を避けるために第1引数にthisのコンテキストを指定することで、指定したものと実行時コンテキストが異なる場合にはコンパイル時にエラーが出る(実行時エラーを避けられる)。

const copyFunc:(this: User) => void = u1.sayName;
// コンパイルエラー:The 'this' context of type 'void' is not assignable to method's 'this' of type 'User'. 
copyFunc();

ちなみに、通常の引数は第2引数以降に指定し、呼び出す側は第1引数のthisは無視して良い。

class User {
  name: string = "taro";
  
  // 呼び出し側の第1引数はageに渡る(thisは無視される)
  sayName(this: User, age: number) {
    console.log(this.name, age);
  }
}

const u1 = new User();
// 出力:"taro", 30 
u1.sayName(30);

オブジェクトの分割代入時、2種類のデフォルト値をどちらも指定する際の挙動

前提として、オブジェクトの分割代入にデフォルト値を指定する方法2つをおさらい。

1. 各プロパティにそれぞれ指定する方法

type User = {
  // デフォルト値を指定するプロパティの型はオプショナル指定必須(引数のオプジェクトに該当プロパティが無い場合にコンパイルエラーとなるため)
  name?: string;
  age?: number;
};

function myself({ name = "anonymous", age = 30 }: User) {
  console.log(name, age);
}

// 出力:anonymous 30
myself({});
// 出力:taro 30
myself({ name: "taro" });

// 上記は特定プロパティがない場合のデフォルト値のため、引数オブジェクト自体を渡さない場合はエラーになる。
// コンパイルエラー:Expected 1 arguments, but got 0.
myself();

2. 引数オブジェクト自体にデフォルト値を指定する方法

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

function myself({ name, age }: User = { name: "anonymous", age: 30 }) {
  console.log(name, age);
}

// 出力:anonymous 30
myself();
myself(undefined);

// 以下の例は引数自体は渡しているため、引数自体のデフォルト値は使用されない
// 出力:undefined undefined
myself({});
// 出力:taro undefined
myself({ name: "taro" });

上記2種類のデフォルト値を組み合わせた場合、以下のようになる。

  • 引数自体を渡さない場合は、引数自体のデフォルト値を使用する
  • 引数のオプジェクトのプロパティの一部が無い場合は、そのプロパティに指定したデフォルト値を使用する
type User = {
  name?: string;
  age?: number;
};

function myself(
  { name = "property_default", age }: User = {
    name: "object_default",
    age: 30,
  }
) {
  console.log(name, age);
}

// 引数のデフォルト値を使用
// 出力:object_default 30
myself();
myself(undefined);

// 引数のデフォルト値は使用しない(プロパティごとに設定したデフォルト値のみ使用)
// 出力:property_default undefined
myself({});
// 出力:taro undefined
myself({ name: "taro" });

メソッドのオーバーライド時、アクセス修飾子は緩める方向にのみ変更可能

class Parent {
  protected doNothing(): void {}
}

class PublicChild extends Parent {
  // protected → publicなのでOK
  public doNothing(): void {}
}

class PrivateChild extends Parent {
  // protected -> privateなのでコンパイルエラー
  // Class 'PrivateChild' incorrectly extends base class 'Parent'. Property 'doNothing' is private in type 'PrivateChild' but not in type 'Parent'.
  private doNothing(): void {}
}

Interfaceのオーバーライドでは、継承元の型に代入できる型のみ指定可能

interface Anonymous {
  name: string;
  age: number | undefined;
}

// name, ageいずれも継承元に代入できるのでOK
interface Person extends Anonymous {
  name: "taro" | "jiro";
  age: number;
}

// コンパイルエラー(以下、nameのエラーだがageも同様):
// Interface 'InvalidPerson' incorrectly extends interface 'Anonymous'.
// Types of property 'name' are incompatible.
// Type 'number' is not assignable to type 'string'.
interface InvalidPerson extends Anonymous{
  name: number;
  age: null;
}

InterfaceではMapped Typesが使えない

type Skill = "go" | "php"

// typeでは使用可能
type SkillType = {
  [key in Skill]: string
}

// interfaceではコンパイルエラー:A mapped type may not declare properties or methods.
interface SkillIF {
  [key in Skill]: string
}

Mapにおいて上書きされるキーとそうでないキー

基本は厳密等価演算子で判定されるけど、NaN同士だけ例外的に同一キーと見做される(NaN === NaNはfalse)

// objectやsymbolはそれぞれ異なるキーと認識されるため、上書きされない
const objectMap = new Map<object, string>([
  [{}, "A"],
  [{}, "B"]
]);
// 出力:Map (2) {{} => "A", {} => "B"}
console.log(objectMap);

const symbolMap = new Map<symbol, string>([
  [Symbol(), "A"],
  [Symbol(), "B"]
]);
// 出力:Map (2) {Symbol() => "A", Symbol() => "B"} 
console.log(symbolMap);

// 以下は全て2つ目のMapで上書きされる
const nullMap = new Map<null, string>([
  [null, "A"],
  [null, "B"]
]);
// 出力:Map (1) {null => "B"}
console.log(nullMap);

const undefinedMap = new Map<undefined, string>([
  [undefined, "A"],
  [undefined, "B"]
]);
// 出力:Map (1) {undefined => "B"}
console.log(undefinedMap);

 const NaNMap = new Map<number, string>([
  [NaN, "A"],
  [NaN, "B"]
]);
// 出力:Map (1) {NaN => "B"} 
console.log(NaNMap);

Mapは直接JSONに変換できない

エラーにはならないが空のJSONになるため、一度オブジェクトにしてからJSONにする必要がある。

const map = new Map<string, string>([
  ["name", "taro"],
  ["age", "30"]
])

// 出力:{}
console.log(JSON.stringify(map));

// 出力:"{"name":"taro","age":"30"}" 
console.log(JSON.stringify(Object.fromEntries(map)));

正規表現の書き方には2通りある

よく見るリテラルを使用した方法だけでなく、クラスのコンストラクタで宣言する方法もある。
コンストラクタで書く場合、バックスラッシュは2回続けて書く必要がある点に注意。

// リテラルでの書き方
const regexpLiteral = /0(8|9)0-\d{4}-\d{4}/g;

// クラスのコンストラクタでの書き方。フラグは第2引数に書く。
const regexpNew = new RegExp("0(8|9)0-\\d{4}-\\d{4}", "g");
// 出力:true
console.log(regexpNew.test("080-1111-2222"))

// \d部分でバックスラッシュを2度続けて書いていないので、正しく一致しない。
const regexpInvalid = new RegExp("0(8|9)0-\d{4}-\d{4}", "g");
// 出力:false
console.log(regexpInvalid.test("080-1111-2222"))

keyofの細かい挙動

string型のキーを持つインデックス型に使用すると、 string | numberのユニオン型になる。
これはnumber型で定義したキーもstring型に変換されるため。

type A = {[k:string]: string};

// 出力:type B = string | number
type B = keyof A;

anyへのkeyof

// type keyofAny = string | number | symbol
type keyofAny = keyof any;

空のオブジェクトへのkeyof

// type keyofEmpty = never
type keyofEmptyObj = keyof {}

型のインデックスへのアクセスから様々な型を生成できる

オブジェクトの型へのインデックへのアクセスは、そのプロパティの型になる。
ユニオン型でのアクセス、kyeofによるアクセスも可能。

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

// type UserNameProp = string
type UserNameProp = User["name"];

// type UserProps = string | number
type UserProps = User["name" | "age"];

// type UserKeyofProps = string | number
type UserKeyofProps = User[keyof User];

配列の型へのインデックスアクセスは、その要素の型になる。
ユニオン型の配列の型へのアクセスも同様。
ちなみに、タプル型でなければnumberの部分は適当な数字でもアクセスできる。

type StringArray = string[];

// いずれも type T = string
type T = StringArray[number];
type T = StringArray[1000];

type stringOrUndefinedArray = (string | undefined)[];
// type U = string | undefined
type U = stringOrUndefinedArray[number];

タプル型の場合は、インデックスに指定した要素の型を取得する。

type UserTuple = ["taro", "jiro"];

// type A2 = "taro" | "jiro"
type T = UserTuple[number];
// type A3 = "taro"
type U = UserTuple[0];
// コンパイルエラー:Tuple type 'UserTuple' of length '2' has no element at index '1000'.
type R = UserTuple[1000];

(型ではなく)配列から型を生成することも可能。

// 型ではないので注意
const mixedArray = ["taro", 100, undefined];

// type T = string | number | undefined
type T = typeof userArray[number];

1つのInterfaceを実装と型引数の制約に使い分ける

考えてみれば当然だけど、interfaceには2つの使い道がある。

  • classのimplement宣言でinterfaceを実装する
  • 型変数でinterfaceをextendsすることで、型引数がそのinterfaceの型である(implementしている)ことを強制する
interface Person {
  name: string;
}

// TはPerson interfaceを満たす型(Person interfaceを実装している型)
// 同時にMyselfもPerson Interfaceを実装している
class Myself<T extends Person> implements Person {
  name = "taro";
  constructor(public partner: T) {}
}

// FriendクラスはPerson interfaceを実装しているのでOK
class Friend implements Person {
  constructor(public name: string) {}
}
new Myself<Friend>(new Friend("Jiro"));

// DogクラスはPersonを実装していないためコンパイルエラー
class Dog {
  constructor(public nickname: string) {}
}
// Type 'Dog' does not satisfy the constraint 'Person'.
// Property 'name' is missing in type 'Dog' but required in type 'Person'.
new Myself<Dog>(new Dog("Pochi"));

// CatクラスはPerson interfaceを実装していないが、nameプロパティを持っておりPerson interfaceと同じ構造のため、
// TypeScriptが採用している構造的部分型の要件を満たしておりエラーにならない
class Cat {
  constructor(public name: string) {}
}
new Myself<Cat>(new Cat("tama"));

delete演算子はオプショナルプロパティにしか使えない

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

const u: User = {
  name: "taro",
  age: 30,
};

// オプショナルなのでOK
delete u.age;

// コンパイルエラー:The operand of a 'delete' operator must be optional.
delete u.name;

同名の値と型のimportは共存可能

type(もしくはinterface)と同名の関数「User」を定義してexportする。

user.ts
export type User = {
  name: string;
  age: number;
};

export const User = (name: string, age: number): User => ({ name, age });

「User」をimportすると、衝突せずにtype(interface)としても関数としても使用可能。

main.ts
import { User } from "./user";

// 1つ目は型、2つ目は関数
const taro: User = User("taro", 30);
87
78
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
87
78