Open5

Typescript 勉強ログ

hsmonhsmon

class

  • TypeScript 上での Class は、インスタンスを生成するだけではなく、Class そのものの型も同時に表すことができる(this を参照するときに、Class そのものを参照できる)
  • contractor の引数に「private」「public」修飾子を付与することにより field の宣言や constructor 内の初期化の記述を省略できる
class Person {
  public name: string;
  private age: number;
  constructor(initName: string, initAge: number) {
    this.name = initName;
    this.age = initAge;
  }
}

↓  以下のように省略可能になる

class Person {
  constructor(public name: string, private age: number) {}
}

  • readonly は読んで字の如く、読み込み専用の修飾子(名前や ID などの書き換えをあまり行わない値に付与する)
    readonly を付与することにより、書き換えによる上書きを防ぐことができる
    ただし、constructor 関数内は初期化の処理になるので、上書きが可能。
class Person {
  constructor(public readonly name: string, private age: number) {}
  changeName() {
    this.name = "hoge"; // ← Error!!
  }
}

  • protected 修飾子を付与することにより、private な状態を保ちつつ extends 先の class の値は参照できるようにする
class Person {
  constructor(public readonly name: string, protected age: number) {}
}
class Teacher extends Person {
  constructor(name: string, age: number, public subject: string) {
    // extendsする場合は、super関数が必要
    super(name, age);
  }
  // 関数を上書きする場合は、関数をそのまま記述する必要がある
  greeting() {
    // Personのthis.ageが**private**だと、下記のthis.ageが**エラー**に
    // Personのthis.ageが**protected**だと、下記のthis.ageは**エラーにならない**
    console.log(
      `Hello, my name is ${this.name}. I'm ${this.age} years old. I teach ${this.subject}`
    );
  }
}

  • getter と setter は同じ関数名でも OK
  • setter は getter と同じ名前にすることにより、引数の値を型推論してくれるようになる
  • getter と同じ名前にすることにより、引数の値を型推論してくれるようになる(逆に同じ名前だと、getter と setter の型は同じじゃないとエラーになる。(引数の型を対象外にするとエラーが出る))
class Teacher extends Person {
  // getterは値を返す必要がある
  get subject() {
    if (!this._subject) {
      throw new Error("There is no subject.");
    }
    return this._subject;
  }
  // getterとsetterは同じ関数名でもOK
  // setterは引数を最低一つ与える必要がある
  // 以下の関数はclassのスコープ外で値の代入する際に処理される ※1
  // getterと同じ名前にすることにより、引数の値を型推論してくれるようになる(逆に同じ名前だと、getterとsetterの型は同じじゃないとエラーになる。(引数の型を対象外にするとエラーが出る))
  set subject(value) {
    if (!value) {
      throw new Error("There is no subject.");
    }
    this._subject = value;
  }
  constructor(name: string, age: number, private _subject: string) {
    // extendsする場合は、super関数が必要
    super(name, age);
  }
  // 関数を上書きする場合は、関数をそのまま記述する必要がある
  greeting() {
    console.log(
      `Hello, my name is ${this.name}. I'm ${this.age} years old. I teach ${this._subject}`
    );
  }
}

const teacher = new Teacher("teach", 20, "Math");
// ※1 ここでsetterが処理される
teacher.subject = "Music";
console.log(teacher.subject);

  • static はインスタンスを生成しないので、this の参照先からも外れる
  • static から this は参照できないので、参照したい場合はそのまま class 名から参照する
class Person {
  // staticはインスタンスを生成しないので、thisの参照先からも外れる
  // もし参照したい場合は、class名(今回であればPerson)をそのまま参照する // ○ Person.species ✗ this.species
  static species = "Homo Sapiens";
  static isAdult(age: number): boolean {
    if (age > 17) return true;
    return false;
  }
  constructor(public readonly name: string, protected age: number) {}
}

  • abstract クラスは TypeScriptのみの機能
  • abstract  = 抽象
  • abstract クラスはインスタンスを生成できない(extends 時のみ利用できる)
  • abstract クラスはインスタンス生成ができないが、static は使える
  • 基本的に abstract クラスは継承のみに使う

  • constructor に private 修飾子を付与することができる(シングルトンパターンを利用するとき)
  • シングルトンパターンは、デザインパターンのひとつで、クラスからひとつしかインスタンスを生成できないようにすること

まとめ(キーワード)

  • field
  • property
  • method
  • constructor
  • method の第一引数は this をとることができる
  • class はインスタンスを生成する設計図にもなるが、TypeScript の場合、型としても機能する
  • public
  • private
  • readonly
  • protected
  • 初期化の省略
  • extends
  • getter
  • setter
  • static
  • abstract
  • private constructor(シングルトンパターン)
hsmonhsmon

interface について

  • interface は端的に表すと、Object の型
  • Type と Interface は殆ど変わらない
  • Interface はObject のみ
  • Type は全部行ける

implements について

  • class で interface を用いるときは、以下のようにimplementsを用いる
interface Human {
  name: string;
  age: number;
  // greeting: (message: string) => void でもOK
  greeting(message: string): void;
}

class Developer implements Human {
  // constructorの引数はprivateだとエラーになる
  constructor(public name: string, public age: number) {}
  greeting(message: string) {
    console.log(message);
  }
}
  • implements は複数指定することができる
  • implements は type でも指定することができる
type Human = {
  name: string;
  age: number;
  // greeting: (message: string) => void でもOK
  greeting(message: string): void;
};

class Developer implements Human {
  // constructorの引数はprivateだとエラーになる
  constructor(public name: string, public age: number) {}
  greeting(message: string) {
    console.log(message);
  }
}
  • implements はあくまで、インスタンスを生成するオブジェクト”**のみ”**影響を与える(static などはインスタンスを生成しないため implements は影響を与えることができない。)

構造的部分型について

  • 変数に変数を代入した場合は、型以外のプロパティがあってもエラーにならない。(変数側まで Typescript は見ない)
  • 変数に直接 Object を代入すると、エラーが出る
interface Human {
	name: string
	age: number
	// greeting: (message: string) => void でもOK
	greeting(message: string):void
}

// implementsは複数指定することができる
class Developer implements Human {
	// implementsはあくまでインスタンスを生成するオブジェクト”のみ”影響を与えるので、staticなどのインスタンスを生成しないものには影響を与えることはできない。
	static id:number = 0
	// constructorの引数はprivate/protectedだとエラーになる
	constructor(public name: string, public age: number, public experience: number) {}
	greeting(message: string) {
		console.log(message)
	}
}
const developer = new Developer("dev",30, 2)
// 変数に変数を代入した場合は、型以外のプロパティがあってもエラーにならない。(変数側までTypescriptは見ない)
// 変数に直接Objectを代入すると、エラーが出る
const Kazuya: Human = developer
const Kazuki: Human = {
	name: "kazuki",
	age: 40,
	**experience: 2, // error!**
	greeting(message: string){
		return message
	}
}

// あくまで、interfaceで定義したプロパティのみしか参照できない。
Kazuya.age

readonly

  • readonly を付与することにより、上書きを禁止することができる
type Human {
	readonly name: string
	age: number
}
const person: Human = {
	name: "foo",
	age: 20
}
person.name = "bar" // ERROR!
  • implements している interface の readonly な値は、constructor 時に初期化される
    なので、public を宣言すればインスタンス生成時、readonly は無効になるし、readonly を指定すれば interface と同様に readonly な値になる
interface Human {
  readonly name: string;
  age: number;
  greeting(message: string): void;
}
class Developer implements Human {
  static id: number = 0;
  // implementsしているinterfaceのreadonlyな値は、constructor時に初期化される
  // なので、publicを宣言すればインスタンス生成時、readonlyは無効になるし、readonlyを指定すればinterfaceと同様にreadonlyな値になる
  constructor(
    public name: string,
    public age: number,
    public experience: number
  ) {}
  greeting(message: string) {
    console.log(message);
  }
}
const developer1 = new Developer("foo", 29, 10);
developer1.name = "aaa";

// interfaceを型指定すると、再びエラーになる
const developer2: Human = new Developer("foo", 29, 10);
developer2.name = "aaa";

extends

  • interface は extends(継承)ができる
  • extends は複数指定できる
interface Namable {
  readonly name: string;
}
interface Human extends Namable {
  age: number;
  greeting(message: string): void;
}
  • 同じプロパティ名を上書きすることは条件が一致するときのみ可能
interface Namable {
	readonly **name**: string // ※1
}

// x(※1) = y(※2) の関係
// 今回の場合は、xはstring型なので、yのstringリテラル型は**代入可能
//** もしyがnumber型などの場合は、代入不可能でErrorが出る
interface Human extends Namable {
	**name**: "foo" // ※2 ←stringリテラル型なのでエラーは出ない。
	age: number
	greeting(message: string):void
}

interface で関数の型を表現する時

  • 基本的には type で関数の型を定義するべきだが、interface でも表現が可能
// 以下のtypeとinterfaceは同じ意味
// type addFunc = (num1: number, num2: number) => number
interface addFunc {
  (num1: number, num2: number): number;
}
const addFunc: addFunc = (n1, n2) => {
  return n1 + n2;
};
hsmonhsmon

インターセクション

  • AかつBのような型を定義したい場合は、「&」を用いる。これをインターセクション型と呼ぶ
  • 以下のようにtypeとinterfaceでは同様の定義が可能
type Engineer1 = {
  name: string
  role: string
}

type Blogger1 = {
  name: string
  followers: number
}

type EnginnerBlogger1 = Engineer1 & Blogger1

// ===================================

// interfaceでも同様の定義が可能
interface Engineer2 {
  name: string
  role: string
}

interface Blogger2 {
  name: string
  followers: number
}

interface EnginnerBlogger2 extends Engineer2, Blogger2 {}
  • Union型TypeとUnion型TypeをIntersection型にすると、ベン図のように合わさった型が優先的に適応される
type NumberBoolean = number | boolean
type StringNumber = string | number
// Union型が複数あるとベン図のように重なっている型が優先される(**今回の場合はnumber型になる**)
type Mix = NumberBoolean & StringNumber

TypeGuard

  • type guardは3種類ある
  • typeof演算子
function toUpperCase(x: string | number){
  **// typeofで型ガードする**
  if(**typeof** x === "string") {
    return x.toUpperCase()
  }
  return ""
}
  • in演算子
type Engineer = {
  name: string
  role: string
}

type Blogger = {
  name: string
  followers: number
}

type NomadWorker = Engineer | Blogger

function describeProfile(nomadWorker: NomadWorker){
  **// 以下のときは、Engineer型とBlogger型の両方にkey名「name」が存在するので、nameにアクセスすることができる(それ以外は不確実なのでアクセスができない)**
  console.log(**nomadWorker.name**)
  **// 以下のif文は引数nomadWorkerにkey名の”role”が含まれていれば、その型全体へアクセスすることができる(以下だと型Engineerにアクセスできる)**
  if("role" **in** nomadWorker) {
    console.log(**nomadWorker.role**)
  }
}
  • instanceof演算子
class Dog {
  speak(){
    console.log("わんわん!")
  }
}

class Bird {
  speak() {
    console.log("ちゅんちゅん")
  }
  fly() {
    console.log("ぱたぱた")
  }
}

type Pet = Dog | Bird

function havePet(pet: Pet) {
  pet.speak()
	// Birdのみアクセスできる
  if(pet **instanceof** Bird) {
    pet.fly()
  }
}

havePet(new Bird())

タグ付きUnion

  • リテラル型を指定することにより、絞り込みが可能になる
class Dog {
  **// タグを指定する(リテラル型を指定)
  kind: "dog" = "dog"**
  speak(){
    console.log("わんわん!")
  }
}

class Bird {
  **kind: "bird" = "bird"**
  speak() {
    console.log("ちゅんちゅん")
  }
  fly() {
    console.log("ぱたぱた")
  }
}

type Pet = Dog | Bird

function havePet(pet: Pet) {
  **// class側にタグを付与することにより、以下のようなアクセスの方法ができるようになる。
  switch(pet.kind) {
    case "bird":
      pet.fly()
      return
    case "dog":
      pet.speak()
      return
  }**
}

havePet(new Bird())
  • 上記のようなclassだけではなく、通常のTypeをつかってリテラル型を指定すれば同じような処理ができる
interface Foo {
	kind: "test"
	name: string
}

let foo: Foo;

function tweet(el: Foo) {
	switch(el.kind) {
    case "test":
      console.log("bar")
  }
}

型アサーション

  • 型アサーションとは、手動で型を上書きすること
  • 以下の場合だとinputの型は「HTMLElement | null」になる
const input = document.getElementById("input")
**// 例えば、inputタグのvalueを書き換えたいときにこのままだとエラーが出てしまう**
input.value = "foo" // ts error:プロパティ 'value' は型 'HTMLElement' に存在しません
  • そのためいかのように型アサーションを利用し、型を上書きする(どちらでもOK)
    ただし、Reactを使う場合は、asを用いたほうが好ましい
const input = <HTMLInputElement>document.getElementById("input")
input.value = "foo"
const input = document.getElementById("input") as HTMLInputElement
input.value = "foo"

!(Non-null assertion operator)を使い、nullじゃないと言い切る

  • 値の末尾に**!**を付与するとnullじゃないと宣言できる
const input = document.getElementById("id")! // null型が外れ、HTMLElement型のみ残る
input.nodeValue = "foo"

インデックスシグネイチャ

  • 以下のような記述をすることで、柔軟に型を扱うことができるようになる
interface Designer {
	name: string // インデックスシグネイチャがstringの場合、それ以外のプロパティもstringに指定しなければならない。
	[index: string]: string // *1
}

const Quill: Designer = {
	name: "quill",
	// *1(インデックスシグネイチャ)を追加することにより、以下も追加できるようになる
	test: "foobar"
}
  • ただし、インデックスシグネイチャを用いることにより、インデックスシグネイチャで指定している型に他のプロパティ名も合わせる必要がある。
  • またインデックスシグネイチャを利用することになり、プロパティのアクセスが厳密ではなくなってしまい、関係のないプロパティのkey名を入力してもエラーが表示されないので注意が必要である
interface Designer {
	name: string // インデックスシグネイチャがstringの場合、それ以外のプロパティもstringに指定しなければならない。
	[index: string]: string // *1
}
const Quill: Designer = {
	name: "quill",
	// *1(インデックスシグネイチャ)を追加することにより、以下も追加できるようになる
	test: "foobar"
}
console.log(Quill.age) // **関係のないkey名にアクセスしてもエラーにならないので注意**

オーバーロード

  • オーバーロードは上から順に評価される
function toUpperCase(x: string):string // オーバーロード
function toUpperCase(x: number):number // オーバーロード
function toUpperCase(x: string | number){
  if(typeof x === "string") return x.toUpperCase()
  return x
}
// オーバーロードを記載しないと、型推論でstring | numberのUnion型になってしまう
const upperHello = toUpperCase("hello")
  • 関数型のオーバーロードは、interfaceで定義する必要がある
interface tmpFunc {
	(x: string): number
	(x: number): number
}
const upperHello: tmpFunc = function (x: string | number) {return 0}
  • 関数と関数のインターセクション型はオーバーロード(上書き)となる
interface FuncA {
  (a: number, b:string): number
  (a: string, b:number): number
}

interface FuncB {
  (a: string): number
}

// 先に指定した(&より左側)型が優先される
let intersectionFunc: FuncA & FuncB
// インターセクション型なので、引数部分は全部の型に適応させる必要がある
intersectionFunc = (a: number | string, b?: number | string) => {
  return 0
}

関数型のユニオン型

  • 関数と関数のユニオン型だと、**パラメータはインターセクション型(1)、戻り値はユニオン型(2)になる

(*1)... 関数のunion型の引数は、string型が入るかもしれないし、number型が入るかもしれない。すなわち引数に何が入るかわからないので以下の場合はnever型となる。number型にstring型はエラーになる。その反対も然り🐥

interface FuncA {
  (a: number): number
}

interface FuncB {
  (a: string): string
}

let unionFunc: FuncA | FuncB
// ↓そのまま関数を実行したい時、このときの型は以下になる
// (a: never(***1**)) => string | number(***2**)
unionFunc()

// 型は「FuncA | FuncB」なので、どちらかの関数を代入することは可能。
unionFunc = function(a: number) {return 0} // FuncAの型が通る
unionFunc = function(a: string) {return "foo"} // FuncBの型が通る

Optional Chaining

  • **?.**を用いることにより、値がfalsyな場合はundefinedやnullを返し、truethyな値の場合はそのプロパティを返す
interface DowloadData {
  id: number
  user?: {
    name?: {
      first: string
      last: string
    }
  }
}

const downloadData: DowloadData = {
  id: 22
}

console.log(downloadData.user?.name?.first)

Nullish Coalescing

  • ??を活用することで、undefinedとnullだった時のみ右側の値が返される
// downloadData.userがundefinedとnullの場合は、"no-user"が返る
const downloadUser = downloadData.user **??** "no-user"

Lookup型

  • 以下のようにinterfaceのプロパティを新しくtypeを作るときにLookupを利用することができる
interface DowloadData {
  id: number
  user: {
    name?: {
      first: string
      last: string
    }
  }
}

**type name = DowloadData["user"]["name"]**
**type profile = DowloadData["id" | "user"]**

型の互換性

  • 詳細は以下を確認

microsoft/TypeScript

レストパラメータ

  • rest parametaにタプルを適応できる
  • 型にタプルを指定すると、指定している数までしか引数を渡せない
// 型にタプルを指定すると、指定している数までしか引数を渡せない
function foo(...args:[string,number,boolean]) {
}
foo("bar",0,true)
  • タプルの場合は可変長引数の指定ができる(1つまで)
function bar(...args:[string,number,boolean, ...number[]]) {
}
bar("foo",0,true, 3,3,3,3,3)
  • タプルや配列ならreadonlyも付与できる
function foobar(...args: readonly string[]) {
	args.push() // ERROR!
}

constアサーション

  • as constで型を定数指定ができるようになる(変更が効かなくなる)
const milk = "milk" // milk: "milk"
let drink = milk // drink: string ←letで変数宣言しているから

const milk = "milk" as const // milk: "milk"
let drink = milk // drink: "milk"
  • 配列にconstアサーションを入れると、readonlyになる
const array = [10,20] as const // const array: readonly [10, 20]
  • オブジェクトもreadonlyに
const peter = {
  name: "Peter",
  age: 29
} as const

// const peter: {
//    readonly name: "Peter";
//    readonly age: 29;
// }

型の中でtypeof

  • 定数などの値をそのまま型にしたいときは、typeofを利用することで表現することができる
const peter = {
  name: "Peter",
  age: 29
} as const

type PeterType = typeof peter
hsmonhsmon
  • 型にも引数を使うことができる。このことをジェネリクスと呼ぶ。
  • ジェネリクスは 「function name<T>(arg: T):T {}」のように表現する
function copy<T>(value: T): T {
  return value;
}
// 引数と戻り値にジェネリクスを指定しているので、型推論してくれる
console.log(copy({ name: "Quil" }));

型パラメーターに制約を付ける方法

  • <T extends {foo: string}>のように extends を用いる
  • ニュアンスは class の implements に近い
function copy<T extends { name: string }>(value: T): T {
  return value;
}

keyof 演算子

  • オブジェクトの key 名を取得したいときは、keyof 演算子を用いる
// type K = "name" | "age"
type K = keyof { name: string; age: number };
  • ジェネリクスと組み合わせてよく使われる
function copy<T extends { name: string }, U extends keyof T>(
  value: T,
  key: U
): T {
  // オブジェクトに動的にアクセスするときに用いる
  value[key];
  return value;
}
console.log(copy({ name: "Quill", age: 98 }, "age"));

class にジェネリクスを用いる時

  • Class でもジェネリクスは利用できる。以下のように表現
class LightDatabase<T extends string | number | boolean> {
  private data:T[] = []

  add(item:T) {
    this.data.push(item)
  }
  remove(item:T) {
    this.data.splice(this.data.indexOf(item),1)
  }
  get() {
    return this.data
  }
}
const stringLightDatabase = new LightDatabase<string>()l
stringLightDatabase.add("Apple")
stringLightDatabase.add("Banana")
stringLightDatabase.add("Grape")
stringLightDatabase.remove("Banana")
console.log(stringLightDatabase.get())

interface に用いる時

  • interface にも Generics は活用できる
interface TmpDatabase<T> {
  id: number;
  data: T[];
}

const tmpDatabase: TmpDatabase<number> = {
  id: 1,
  data: [1, 2, 3],
};
  • type も同様
type TmpDatabase<T> = {
  id: number;
  data: T[];
};

Utility 型について

  • Utility 型とは、「型のライブラリ」
interface Todo {
  title: string
  text: string
}
// Partialはkeyを全部Optionalに変更するUtility型(TypeScriptにもともと備わっているライブラリ)
type Todoable = Partial<Todo>type Todoable = {
    title?: string | undefined;
    text?: string | undefined;
}
interface Todo {
  title: string
  text: string
}

type ReadTodo = Readonly<Todo>// keyをすべてreadonlyに
type ReadTodo = {
    readonly title: string;
    readonly text: string;
}
  • Promise は最初からジェネリクス型なので、型を指定するのが Better
const fetchData: Promise<string> = new Promise((resolve) => {
  setTimeout(() => {
    resolve("hello");
  }, 1000);
});

fetchData.then((data) => {
  console.log(data.toUpperCase());
});
  • 配列にも、ジェネリクスの指定が可能
// string[]とおなじ
const vegetables: Array<string> = ["tomato", "broccoli"];

デフォルトの型パラメーターを指定したい時

  • ジェネリクスにもデフォルトで型パラメーターを設定できる(<T = 型>)
interface ResponseData<T = any> {
  data: T;
  status: number;
}

// デフォルト値が設定されているのでエラーにならない
let tmp: ResponseData;

Mapped Types(型の for 文)

  • Mapped Types は型の for 文
  • Mapped Types はオブジェクトである必要がある
type MappedTypes = {
  [P in "tomato" | "pumpkin"]:  P
}type MappedTypes = {
    tomato: "tomato";
    pumpkin: "pumpkin";
}
  • keyof 演算子と組み合わせることもできる
interface Vegetables {
  tomato: string
  pumpkin: string
}

type MappedTypes = {
  [P in keyof Vegetables]:  P
}type MappedTypes = {
    tomato: "tomato";
    pumpkin: "pumpkin";
}
  • readonly も付与できる
type MappedTypes<T> = {
  readonly [P in keyof T]:  P
}type MappedTypes = {
    readonly foo: "foo";
    readonly bar: "bar";
}
  • readonly の前に「-」(ハイフン)を付与することにより readonly を外すことができる
interface Vegetables {
  readonly tomato: string
  pumpkin: string
}

type MappedTypes = {
  **-**readonly [P in keyof Vegetables]:  P
}type MappedTypes = {
    tomato: "tomato";
    pumpkin: "pumpkin";
}
  • optional も付与できる
type MappedTypes<T> = {
  readonly [P in keyof T]?: string
}type MappedTypes = {
    readonly foo?: string | undefined;
    readonly bar?: string | undefined;
}
  • optional も readonly と同様に、?の前に「-」(ハイフン)を付与することにより、optional を外すことができる
interface Vegetables {
  readonly tomato: string
  pumpkin?: string
}

type MappedTypes = {
  -readonly [P in keyof Vegetables]**-?**:  P
}type MappedTypes = {
    tomato: "tomato";
    pumpkin: "pumpkin";
}
  • じつは、Partial や Readonly も MappedTypes を使っている。(ソースを見ればわかる)
/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Conditinal Types(型の if 文)

  • 三項演算子みたいに型を条件分岐で型を代入する
type CoditionalTypes = "tomato" extends string ? number : booleantype CoditionalTypes = number
// stringに"tomato"は入らないので、booleanが返る
type CoditionalTypes = string extends "tomato" ? number : booleantype CoditionalTypes = boolean
  • infer をつかって型推論を扱うことができる(infer は、推測するという意味)
// inter R の Rは引数みたく命名は何でもOK
type ConditionalTypesInfer = {tomato:"tomato"} extends {tomato: infer R } ? R : booleantype ConditionalTypesInfer = "tomato"
  • union 型で Conditional Types を使うときは、Distributive Conditinal Types を活用する
type DistributiveConditionlTypes<T> = T extends "tomato" ? number : boolean
let tmp1: DistributiveConditionlTypes<"tomato" | "pumpkin">// union型のそれぞれの値が評価され、型が返却される。
let tmp1: number | boolean
  • DistributiveConditinalTypes は Utility 型でも扱われている
let tmp2: NonNullable<string | null>let tmp2: string
/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;
/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;
hsmonhsmon

デコレーターを使って Class に関数を適応させる方法

  • デコレーターは関数
  • デコレーターはクラスの定義時に実行される(インスタンスの生成時よりも前)
// 頭文字小文字でもいいが、大文字が通例
// classの本質は、contructorのシンタックスシュガーなので関数になる。そのため引数の型はFunction型になる
// FunctionはDateとかMathとかと同じJavascriptに備わっているもの
function Logging(constructor: Function) {
  console.log("Logging...")
  console.log(constructor)
}

@Logging
class User {
  name = "Foo"
  constructor(){
    console.log("User was created!")
  }
}

const user = new User()// 実行結果
Logging...
[class User]

User was created!

デコレーターファクトリー

  • デコレーターに引数を取りたい場合は、デコレーターファクトリーを利用する(関数の中にデコレーターを返す)
function Logging(message: string) {
  return function (constructor: Function) {
    console.log(message);
    console.log(constructor);
  };
}

@Logging("Logging User")
class User {
  name = "Foo";
  constructor() {
    console.log("User was created!");
  }
}

const user = new User();

デコレーターを使って簡易版のフレームワークを作成する

function Component(template: string, selector: string) {
  // constructor関数を渡して、インスタンスを生成したいときは型を{new(): ...}とする
  // constructor内に引数がある場合は、new()の中にconstructorと同じ引数と型を書く
  // もしくは、スプレッド構文をつかっていくつもの引数を受け取れるようにする
  return function Component(constructor: {
    new (...args: any[]): { name: string };
  }) {
    const mountedElement = document.querySelector(selector);
    const instace = new constructor(23);
    if (mountedElement) {
      mountedElement.innerHTML = template;
      mountedElement.querySelector("h1")!.textContent = instace.name;
    }
  };
}

@Component("<h1>{{name}}</h1>", "#app")
@Logging("Logging User")
class User {
  name = "Foo";
  constructor(age: number) {
    console.log("User was created!");
  }
}

複数のデコレーターを同時に使う

  • デコレーターファクトリーは上から下へ順番に実行される
  • デコレーターは下から上へ順番に実行される
  • 以下サンプルでは、以下のような順番で実行される
function Logging(message: string) {
  console.log("デコレーターファクトリー,順番①");

  return function (constructor: Function) {
    console.log("デコレーターファクトリー,順番④");

    console.log(message);
    console.log(constructor);
  };
}

function Component(template: string, selector: string) {
  console.log("デコレーターファクトリー,順番②");

  return function Component(constructor: {
    new (...args: any[]): { name: string };
  }) {
    console.log("デコレーター,順番③");

    const mountedElement = document.querySelector(selector);
    const instace = new constructor(23);
    if (mountedElement) {
      mountedElement.innerHTML = template;
      mountedElement.querySelector("h1")!.textContent = instace.name;
    }
  };
}

@Logging("Logging User")
@Component("<h1>{{name}}</h1>", "#app")
class User {
  name = "Foo";
  constructor() {
    console.log("User was created!");
  }
}

const user = new User();

戻り値にクラスを指定して、新しいクラスを作り出す方法

function Logging(message: string) {
  return function (constructor: Function) {
    console.log(message);
    console.log(constructor);
  };
}

function Component(template: string, selector: string) {
  // 新しくClassを返す場合は、引数constructorの型はジェネリクスにすることで、すべてのClassに適応できるようになる
  // fieldをいちいち全部記述しなくても、必要なfieldだけ記述すればOKになる
  return function Component<
    T extends { new (...args: any[]): { name: string } }
  >(constructor: T) {
    // 戻り値にクラスを指定して、新しいクラスを作り出す
    return class extends constructor {
      constructor(...args: any[]) {
        super(...args);
        console.log("Component");
        const mountedElement = document.querySelector(selector);
        const instace = new constructor(23);
        if (mountedElement) {
          mountedElement.innerHTML = template;
          mountedElement.querySelector("h1")!.textContent = instace.name;
        }
      }
    };
  };
}

@Component("<h1>{{name}}</h1>", "#app")
@Logging("Logging User")
class User {
  name = "Kazumi";
  constructor(public age: number) {
    console.log("User was created!");
  }
}

const user = new User(2);

プロパティデコレーター

  • デコレーターは Class の一部のプロパティのみ利用することもできる。それをプロパティデコレーターと呼ぶ(プロパティに対してデコレーターする)
  • プロパティデコレーターはクラスデコレーターより、先に実行される
  • field に対し static のときは class を返し、public のときは prototype を返す (※1)
function PropertyLogging(target: any, propertyKey: string) {
  console.log({ target, propertyKey });
  // プロパティデコレーターは値を返せない!
}

@Component("<h1>{{name}}</h1>", "#app")
@Logging("Logging User")
class User {
  @PropertyLogging
  name = "Kazumi"; // (※1)staticのときは、Classを返し、publicのときは、prototypeを返す
  constructor(public age: number) {
    console.log("User was created!");
  }
  greeting() {
    console.log("hello!");
  }
}

const user = new User(2);

メソッドデコレーターを使う & PropertyDescriptor について

  • プロパティデコレーターと同様にメソッドに対しデコレーターを適応できる。
function MethodLogging(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log({ target, propertyKey, descriptor });
}

class User {
  constructor(public age: number) {
    console.log("User was created!");
  }
  @MethodLogging
  greeting() {
    console.log("hello!");
  }
}

PropertyDescriptor とは

  • 以下のような Object の情報に対し、Object.getOwnPropertyDescriptorを利用することにより、プロパティの詳細情報を取得することができる。(プロパティのメタみたいな裏側の情報が取得できる)
const user = {
	name: "foo",
	age: 50
}

Object.getOwnPropertyDescriptor(user,"name"){
value: "foo",
writable: true, // valueの上書き可否
enumerable: true, // for文を利用して回したときに、値として含めるか
configurable: true // value,writable,enumerable,configurable全体の上書き可否(configurable: falseで実行することにより、後からtrueの上書きもできなくなる)
}
  • 実は、user.protoの中には PropertyDescriptor の情報はない
  • PropertyDescriptor はブラウザ側が裏側で持っている情報
  • PropertyDescriptor の値を set するには、Object.definePropertyを利用する
  • ちなみに class の get と set は Object.defineProperty を活用している
  • PropertyDescriptor は値を返すことができる
Object.defineProperty(user,"name",{value: "bar"})

Object.getOwnPropertyDescriptor(user,"name"){
value: "bar",
writable: true,
enumerable: true,
configurable: true
}

// lib.es5.d.ts

interface PropertyDescriptor {
  configurable?: boolean;
  enumerable?: boolean;
  value?: any;
  writable?: boolean;
  get?(): any;
  set?(v: any): void;
}

PropertyDescriptor の get と set

  • getter と setter は value と共存できない
Object.defineProperty(user,"name",{get(){return "Hello"}}){
age: 50,
name: "Hello"
}

上記を踏まえてメソッドデコレーターでは PropertyDescriptor を活用することができる

function MethodLogging(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(descriptor)
}

class User {
  constructor(public age: number){
    console.log("User was created!")
  }
  @MethodLogging
  greeting() {
    console.log("hello!")
  }
}{
writable: true,
enumerable: false,
configurable: true,
enumerable: false,
value: ƒ greeting(),
writable: true,
__proto__: Object,
}

アクセサーデコレーター

function AccessorLogging(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(descriptor)
}

class User {
  constructor(private _age: number){
    console.log("User was created!")
  }

  @AccessorLogging
  get age() {
    return this._age
  }
  set age(value) {
    this._age = value
  }
}

const user = new User(2){
enumerable: false,
configurable: true,
enumerable: false,
get: ƒ age(),
set: ƒ age(value),
__proto__: Object
}

戻り値を使って実践的なメソッドデコレーターを使う

  • PropertyDescriptor は値を返すことができる
function enumerable(isEnumerable: boolean) {
  return function (
    _target: any,
    _propertyKey: string,
    _descriptor: PropertyDescriptor
  ) {
    return {
      enumerable: isEnumerable,
    };
  };
}
function AccessorLogging(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log(descriptor);
}

class User {
  constructor(private _age: number) {
    console.log("User was created!");
  }
  @enumerable(false) // enumerableをfalseに
  @AccessorLogging
  get age() {
    return this._age;
  }
  set age(value) {
    this._age = value;
  }
}

パラメーターデコレーター

  • class 内のメソッドの引数にデコレーターを利用することができる
  • パラメーターデコレーターの場合、第 3 引数が異なるので注意
  • 第 3 引数は、パラメーターデコレーターを付与している引数の順番が何番目に属するかを返してくれる
function ParameterLogging(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  console.log(parameterIndex); // 1
}
class User {
  constructor(private _age: number) {
    console.log("User was created!");
  }
  @MethodLogging
  greeting(_text: string, @ParameterLogging message: string) {
    console.log("hello!");
  }
}

デコレーターの活用例