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.
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
Just dumping the latest version of this here, enjoy comments and secrets.
require "json"
EOL = "\n"
annotation TypeScriptComment
end
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
@[TypeScriptComment("bar.ts")]
class Bar
include JSON::Serializable
# id property
property id : Int32
@[TypeScriptComment("dname is short for display name")]
property dname : String?
@[TypeScriptComment("the abc3 property is important for our customers")]
property abc3 : String?
def initialize(@id : Int32)
end
end
@[TypeScriptComment("foo.ts")]
@[TypeScriptComment("Foo is about it")]
class Foo
include JSON::Serializable
property id : Int32
@[TypeScriptComment("dname is short for display name")]
property dname : String?
property bars : Array(Bar)
@[JSON::Field(ignore: true)]
property secrets : Array(String)?
def initialize(@id : Int32, @bars = Array(Bar).new)
end
end
def get_tsdef(_klass : T.class, io : IO) forall T
{% if T.annotations(TypeScriptComment).size > 1 %}
io << "/**" << EOL
{% for ann, idx in T.annotations(TypeScriptComment) %}
# io << "Annotation {{ idx }} = {{ ann[0].id }}"
io << " * " << "{{ ann[0].id }}" << EOL
{% end %}
io << " */" << EOL
{% elsif T.annotations(TypeScriptComment).size == 1 %}
{% for ann, idx in T.annotations(TypeScriptComment) %}
io << "// " << "{{ ann[0].id }}" << EOL
{% end %}
{% end %}
io << "export type #{{{ T.name }}} = {" << EOL
indent = " "*2
{% for value, idx in T.instance_vars %}
{% unless value.annotations(JSON::Field).select { |jf| jf[:ignore] || jf[:ignore_serialize] }.size > 0 %}
{% if value.annotations(TypeScriptComment).size > 1 %}
io << indent << "/**" << EOL
{% for ann, idx in value.annotations(TypeScriptComment) %}
# io << indent << "Annotation {{ idx }} = {{ ann[0].id }}"
io << indent << " * " << "{{ ann[0].id }}" << EOL
{% end %}
io << indent << " */" << EOL
{% elsif value.annotations(TypeScriptComment).size == 1 %}
{% for ann, idx in value.annotations(TypeScriptComment) %}
io << indent << "// " << "{{ ann[0].id }}" << EOL
{% end %}
{% end %}
io << indent << {{ value.name.stringify }} << " : " << typenode_to_typescript_type_string({{ value.type.union? ? value.type.union_types : value.type }}) << ","
io << EOL
{% end %}
{% end %}
io << "}" << EOL
end
get_tsdef Bar, STDOUT
get_tsdef Foo, STDOUT
output
// bar.ts
export type Bar = {
id : number,
// dname is short for display name
dname : string | null,
// the abc3 property is important for our customers
abc3 : string | null,
}
/**
* foo.ts
* Foo is about it
*/
export type Foo = {
id : number,
// dname is short for display name
dname : string | null,
bars : Array<Bar>,
}
changes:
- comments can be written in the crystal and still be exported to typescript.
- the exporter will skip properties that JSON.Field says to ignore.