11
2

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 5 years have passed since last update.

flowtypeのclassとobject typeについて整理する

Last updated at Posted at 2018-06-06

flowのclass/object型の対応が独特でハマったのでメモ。

前提 - nominalとstructuralな型付け

一般的な型システムには以下の二種類が存在する。

nominal typing

明示的にclass/interfaceを宣言し、それを元に型を判定する。
java/C#などがコレ。

structural typing

objectのキーに注目し、それが一致するかで型を判定する。
typescript、golangなどがコレ(らしい)。

flowの型システムには、その二種類の型付けが混在している。

Class - nominal typing

Classはnominalな型付けで判定される。

class User {
  id: string
}
class GuestUser extends User {
  id: string
  expire: string
}
class AdminUser {
  id: string
  password: string
}

const guest :User = new GuestUser() // work
const admin :User = new AdminUser() // error!  

java/C#などを触っていれば、特に違和感無いと思います。

Object type - structural typing

typeで宣言されるobject typeは、structuralな判定が行われる。

type UserParam = {
  id: string
}
type GuestUserParam = {
  id: string,
  expire: string
}
type AdminUserParam = {
  id: string,
  password: string
}

const guest :GuestUserParam = {
    id: 'id-1',
    expire: '2018-06-06T14:00:05.182Z'
}
const admin :AdminUserParam = {
    id: 'id-2',
    password: 'passw0rd!'
}

const userA :UserParam = guest // work
const userB :UserParam = admin  // work

これもflowの資料みていると違和感ありませんね。

Object type と class

この二つが混ざるとちょっと妙な動きをします。

class AdminUser {
  id: string
  password: string
}

type AdminUserParam = {
  id: string,
  password: string
}

const admin :AdminUserParam = {
    id: 'id-2',
    password: 'passw0rd!'
}

const userA :AdminUser = admin // error!

classで宣言された型は「インスタンスがそのクラスorサブクラスによって生成された型か」を判定するため、フィールドの値が同じでも型判定が失敗します。

class AdminUser {
  id: string
  password: string
  constructor(params){
    this.id = params.id
    this.password = params.password
  }
}

const userA :AdminUser = new AdminUser({id: '',
 password: ''})
const userB :AdminUser = Object.assign({},userA,{id: 'test'}) // error!
const userC :AdminUser = new AdminUser(Object.assign({},userA,{id: 'test'})) //work!

userBのような、Object.assignによるオブジェクトの更新も、classから生成されていないオブジェクトなのでエラーが出ます。
コレは結構紛らわしいですね。
userCのようにconstructorにもう一度つっこむと正常にパースされて型がつきます。オブジェクトスプレッドと合わせてnew AdminUser({...userA,id: 'test'})とか書くと見た目も良いかと思われます。
この特徴から、クラスを作ると以下のような運用になることが多いです。

AdminUser.js
export default class AdminUser {
  id: string
  password: string

  constructor(params: AdminUserParam){
    this.id = params.id
    this.password = params.password
  }
}

export type AdminUserParam = {
  id: string,
  password: string
}

ちょっと冗長ですね。もっといいやり方あったら教えてください。

ちなみに

typescriptはclassのインスタンスもstructuralに型付けがされるため、以下のようになります。

class AdminUser {
  id: string
  password: string
  constructor(params){
    this.id = params.id
    this.password = params.password
  }
}

const userA :AdminUser = new AdminUser({id: '',
 password: ''})
const userB :AdminUser = Object.assign({}, userA, {id: 'test'}) // work!
const userC :AdminUser = {id: '', password: ''} //work

上のようなややこしさが無くシンプルですね。
ただ、コレはコレでピュアオブジェクトとクラスインスタンスを型で区別できないという問題があったりします。一長一短ですね。

追記

@kinzal 様より、以下のようにすると重複なしでかけるとのコメントをいただきました!

AdminUser.js
export default class AdminUser {
  id: string
  password: string

  constructor(params: AdminUserParam){
    this.id = params.id
    this.password = params.password
  }
}

export type AdminUserParam = $Diff<AdminUser, {}>

ただ、これを使って集約したクラスを表現したところ、また悩みにブチ当たったので記事書きました。
次の記事はこちら

11
2
3

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
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?