LoginSignup
3
0

More than 5 years have passed since last update.

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

Posted at

やりたいこと

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
        ]
      }
    ]
  }
}

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

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