flowtype

flow の型定義からそれを満たすインスタンスを作る

やりたいこと

storybook のカタログを flow から自動生成できないか、と思って調べていた。

こんなコードがあったとする。

index.js
/* @flow */

type Point = {
  x: number,
  y: number
}

type Props = {
  a: string,
  p: Point,
  c: {
    d: { e: any }
  },
  arr: Array<number>
}

const props: Props = {
  a: 'foo',
  p: { x: 1, y: 2 },
  c: { d: { e: 3 } },
  arr: [1]
}

export default props

これから props のインスタンスを知らない状態で、型からそれを満たすインスタンスを生成したい。

flow-bin と flow-parser-bin を使って export default されてるオブジェクトの型の推論結果を取得し、そこからインスタンスを再生成する。

/* @flow */
const fs = require('fs')
const { execFileSync } = require('child_process')
const flow = require('flow-bin')
const parser = require('flow-parser-bin')
const debug = obj => console.log(JSON.stringify(obj, null, 2))

function getTypeAtPos(fpath: string, line: number, column: number) {
  const ret = execFileSync(flow, [
    'type-at-pos',
    fpath,
    `${line}`,
    `${column}`,
    '--json'
  ]).toString()
  return JSON.parse(ret)
}

function getExportDefaultType(fpath: string): string | null {
  const body = fs.readFileSync(fpath).toString()
  const ast = parser.parse(body)
  const exportDefaultNode = ast.body.find(
    node => node.type === 'ExportDefaultDeclaration'
  )

  if (exportDefaultNode) {
    const start = exportDefaultNode.declaration.loc.start
    const { type } = getTypeAtPos(fpath, start.line, start.column + 1)
    return type
  }
  return null
}

function astToObject(node) {
  switch (node.type) {
    case 'ObjectTypeAnnotation': {
      return node.properties.reduce((acc, property) => {
        // TODO: Check key is optional
        return { ...acc, [property.key.name]: astToObject(property.value) }
      }, {})
    }
    case 'GenericTypeAnnotation': {
      if (node.id.name === 'Array') {
        return [astToObject(node.typeParameters.params[0])]
      } else {
        // TODO: Trace type instance
        return {}
      }
    }
    case 'NumberTypeAnnotation': {
      return 42
    }
    case 'StringTypeAnnotation': {
      return '<string>'
    }
    case 'AnyTypeAnnotation': {
      return '<any>'
    }
    default: {
      console.log('missing:', node.type)
      return null
    }
  }
}

const typeExprString = getExportDefaultType('index.js')
if (typeExprString) {
  const typeExpr = parser.parse('type T = ' + typeExprString).body[0].right
  debug(astToObject(typeExpr))
}

実行するとこうなる

{
  "a": "<string>",
  "arr": [
    42
  ],
  "c": {
    "d": {
      "e": "<any>"
    }
  },
  "p": {
    "x": 42,
    "y": 42
  }
}

課題

type-at-pos では Generics の参照を解決しきれていない。

このとき

type Optional<T> = T | null
const t: Optional<string> = null
export default t

AST は Optional 定義を探しにいかないとならない

{
  "type": "GenericTypeAnnotation",
  "loc": {
    "source": null,
    "start": {
      "line": 1,
      "column": 9
    },
    "end": {
      "line": 1,
      "column": 25
    }
  },
  "range": [
    9,
    25
  ],
  "id": {
    "type": "Identifier",
    "loc": {
      "source": null,
      "start": {
        "line": 1,
        "column": 9
      },
      "end": {
        "line": 1,
        "column": 17
      }
    },
    "range": [
      9,
      17
    ],
    "name": "Optional",
    "typeAnnotation": null,
    "optional": false
  },
  "typeParameters": {
    "type": "TypeParameterInstantiation",
    "loc": {
      "source": null,
      "start": {
        "line": 1,
        "column": 17
      },
      "end": {
        "line": 1,
        "column": 25
      }
    },
    "range": [
      17,
      25
    ],
    "params": [
      {
        "type": "StringTypeAnnotation",
        "loc": {
          "source": null,
          "start": {
            "line": 1,
            "column": 18
          },
          "end": {
            "line": 1,
            "column": 24
          }
        },
        "range": [
          18,
          24
        ]
      }
    ]
  }
}

まだやり方がわからないので、わかったら書く。