1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ジェネリクスってめっちゃ便利やん

Posted at

はじめに

typescriptを学び始めた頃私は思いました
「継承とかインターフェースとかジェネリクスっていつ使えばいいんだろう」 と。
ライブラリで使われてるのをよく見かけたりするものの、(ジェネリクスであればReactのPropなど)
はじめの頃は自分でいざ実装するとなるとどこらへんで役に立つのかがよく分かりませんでした。
しかし最近なんとなくわかってきたような気がしたので使い所のようなものを簡単なコード例を用いながらシェアしたいと思います😌

例えば以下のようなクラスがあるとします。

export class Worker {
  private _id: string;
  private _name: string;

  constructor({ id, name }: { id: string; name: string }) {
    this._id = id;
    this._name = name;
  }

  get id(): string {
    return this._id;
  }

  set id(id: string) {
    this._id = id;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._id = name;
  }
}

class Manager {
  private _managerId: string;
  private _name: string;

  constructor({ managerId, name }: { managerId: string; name: string }) {
    this._managerId = managerId;
    this._name = name;
  }

  get managerId(): string {
    return this._managerId;
  }

  set managerId(managerId: string) {
    this._managerId = managerId;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._name = name;
  }
}

ある時WorkerとManagerの配列を一意のプロパティがkeyでインスタンス自体がvalueのMap構造に変換することが必要になりました。
*WorkerクラスのidManagerクラスのmanagerIdはともに一意の値となっていることと仮定ます。

const workerA = new Worker({id: "1", name:"John"})
const workerB = new Worker({id: "2", name:"Michel"})
const workerC = new Worker({id: "3", name:"Daniel"})
const workers: Worker[] = [workerA, workerB, workerC] 

const managerA = new Manager({managerId: "1", name:"Charlie"})
const managerB = new Manager({managerId: "2", name:"Brake"})
const managerC = new Manager({managerId: "3", name:"Richard"})
const managers: Manager[] = [managerA, managerB, managerC]

便宜上インスタンスA,B,Cみたいな感じで作ってますが実務だと例えばworkersはAPI叩いたレスポンスとかになるかもしれません。
一番単純に実装するなら以下の様になるでしょう。

const workerMap = new Map<string, Worker>()
for(const worker of workers) {
    workerMap.set(worker.id, worker)
}

const managerMap = new Map<string, Manager>()
for(const manager of managers) {
    managerMap.set(manager.managerId, manager)
}

ここで2つの処理の類似性に気づきます。リファクタしたい...しかし一意となっているプロパティ名の違いが共通化を阻んでいます。
インターフェース、あるいは継承を行えば一意となるプロパティ名を共通化できます。
はじめの例はインターフェースを用います。

interface CompanyMember {
  id: string;
}

class Worker implements CompanyMember {
  private _id: string;
  private _name: string;

  constructor({ id, name }: { id: string; name: string }) {
    this._id = id;
    this._name = name;
  }

  get id(): string {
    return this._id;
  }

  set id(id: string) {
    this._id = id;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._name = name;
  }
}

class Manager implements CompanyMember {
  private _id: string;
  private _name: string;

  constructor({ id, name }: { id: string; name: string }) {
    this._id = id;
    this._name = name;
  }

  get id(): string {
    return this._id;
  }

  set id(id: string) {
    this._id = id;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._name = name;
  }
}

インターフェースを用いればあるクラスがあるプロパティやメソッドを持つことを強制させることができ、後に処理を共通化しやすくなります。

さあこれでMap化する処理を共通化できそうだと思ったところでもう一つ共通化を阻む問題が出ました。クラスの違いです。

ジェネリクスを使えば型をそのものも引数のように使えます

const createMapFromCompanyMemberArray = <T extends CompanyMember>(array: T[]): Map<string, T> => {
  const result = new Map<string, T>();

  for (const el of array) {
    result.set(el.id, el);
  }

  return result;
};

const workersMap = createMapFromCompanyMemberArray(workers);
const managersMap = createMapFromCompanyMemberArray(managers);

継承させるなら以下のようにしても良いかもしれません。

class CompanyMember {
  private _id: string;

  constructor({ id }: { id: string }) {
    this._id = id;
  }

  get id(): string {
    return this._id;
  }

  set id(id: string) {
    this._id = id;
  }
}

export class Worker extends CompanyMember {
  private _name: string;
  private _manager: Manager;

  constructor({ id, name, manager }: { id: string; name: string; manager: Manager }) {
    super({ id });

    this._name = name;
    this._manager = manager;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._name = name;
  }

  get manager(): Manager {
    return this._manager;
  }

  set manager(manager: Manager) {
    this._manager = manager;
  }
}

export class Manager extends CompanyMember {
  private _name: string;

  constructor({ id, name }: { id: string; name: string }) {
    super({ id });
    this._name = name;
  }

  get name(): string {
    return this._name;
  }

  set name(name: string) {
    this._name = name;
  }
}

おわりに

ジェネリクスやインターフェースという新たな武器を得ることでリファクタリングするのが楽しくなりました!
簡単な例にはなりましたが昔の自分のように悩んでいる方の何か役に立てたら幸いです。

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?