Export your Crystal types to Typescript?

If you’re using Crystal and Typescript, probably you have some structs/classes/enums that would benefit from a matching typescript definition file. Has anymore made anything for this? Maybe automatically generate a typescript definition file? The benefit being that you don’t want to maintain a file that is just a copy of information in your crystal files and would need to be synced.

https://app.quicktype.io/ Is something that comes to mind, but not exactly what you’re thinking of.

2 Likes

Sounds like you might be thinking along the lines of Crystal objects that you serialize into a JSON response and the TypeScript app on the other end of the request needs to be able to deserialize them in a type-safe way.

If that’s your use case, you may want to define the abstract type using something like OpenAPI and generate both Crystal and TypeScript types from that. You might be able to use this Crystal code generator for OpenAPI.

So I’ve got this prototype written:

require "json"

EOL = "\n"

TypeScriptTypeMap = {
  Int32.name => "number",
  String.name => "string",
  Nil.name => "null",
} of String => String

def typenode_to_typescript_type_string(value : Array(::Class)): String
  return value.sort {|a, b| 
    # pp ({:a => a, :b => b, :c => a==Nil, :d => a==String})
    next -1 if b == Nil
    next a.name <=> b.name
  }.map {|v| typenode_to_typescript_type_string(v) }.join " | "
end

def typenode_to_typescript_type_string(value : ::Class): String
  TypeScriptTypeMap[value.name]? || value.name.gsub({
    '(' => '<',
    ')' => '>',
  } of Char => Char)
end

class Bar
  include JSON::Serializable
  property id : Int32
  property dname : String?
  def initialize(@id : Int32, @foo_list : String? = nil)
  end
end

class Foo
    include JSON::Serializable
    property id : Int32
    property dname : String?
    property bars : Array(Bar)
  
  def initialize(@id : Int32, @foo_list : String? = nil, @bars = Array(Bar).new)
  end

  def get_tsdef(io : IO)
    io << "// #{{{ @type.name.stringify.downcase }}}.ts" << EOL
    io << "export type #{{{ @type.name }}} = {" << EOL
    {% for value, idx in @type.instance_vars %}
      io << " "*2 << {{ value.name.stringify }} << " : " << typenode_to_typescript_type_string({{ value.type.union? ? value.type.union_types : value.type }}) << "," << EOL
    {% end %}
    io << "}" << EOL
    nil
  end
end

Foo.new(id: 50).get_tsdef(STDOUT)

output:

// foo.ts
export type Foo = {
  id : number,
  dname : string | null,
  bars : Array<Bar>,
  foo_list : string | null,
}

The main problem, which is almost aesthetic, is that I would like Foo.new(id: 50).get_tsdef(STDOUT) to be more like get_tsdef(Foo, STDOUT). I couldn’t get any thing like instance_vars to return a non-zero length array.

What were you doing exactly? This seems to work:

def get_tsdef(_klass : T.class, io : IO) forall T
	io << "// #{{{ T.name.stringify.downcase }}}.ts" << EOL
    io << "export type #{{{ T.name }}} = {" << EOL
    {% for value, idx in T.instance_vars %}
      io << " "*2 << {{ value.name.stringify }} << " : " << typenode_to_typescript_type_string({{ value.type.union? ? value.type.union_types : value.type }}) << "," << EOL
    {% end %}
    io << "}" << EOL
end


get_tsdef Foo, STDOUT