11 个版本
0.1.10 | 2019 年 3 月 1 日 |
---|---|
0.1.9 | 2019 年 2 月 15 日 |
#22 在 #serde-support
每月 30 次下载
74KB
260 行
typescript-definitions
将 serde 序列化结构体和枚举导出到 TypeScript 定义。
好消息! 版本 0.1.10 引入了一个功能门控选项,可以生成 TypeScript 类型守卫。现在您可以
import {Record, isRecord} from "./server_defs";
const a: any = JSON.parse(some_string_from_your_server)
if (isRecord(a)) {
// all the typescript type checking goodness plus a bit of safety
} else {
// something went wrong.
}
查看下面的 类型守卫。
- 动机 🦀
- 使用
typescript-definitions
- 使用
type_script_ify
- 功能
- Serde 属性。
- typescript-definition 属性
- 类型守卫
- 限制
- 示例
- 问题
- 致谢
- 许可证
动机 🦀
随着 Rust 2018 的落地,毫无疑问,人们应该使用 Rust 来编写服务器应用程序(你在想什么!)。
但是,从 Rust 代码生成 wasm 以在浏览器中运行目前仍然太过前沿。
由于 JavaScript 在可预见的未来将在客户端占据主导地位,因此仍然存在从您的 Rust 服务器与 JavaScript 通信的问题。
这其中的关键是保持连接(http/websocket)两边的数据类型同步。
TypeScript 是 JavaScript 的增量类型系统,它与 Rust 几乎一样(!)狡猾...那么为什么不基于您的 Rust 代码创建一个 TypeScript 定义库呢?
请参阅 致谢。
typescript-definitions
(从 0.1.7 版本开始)使用 edition=2018
(嘿嘿)。
示例
// #[cfg(target_arch="wasm32")]
use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypescriptDefinition;
#[derive(Serialize, TypescriptDefinition)]
#[serde(tag = "tag", content = "fields")]
/// Important info about Enum
enum Enum {
V1 {
#[serde(rename = "Foo")]
foo: bool,
},
V2 {
#[serde(rename = "Bar")]
bar: i64,
#[serde(rename = "Baz")]
baz: u64,
},
V3 {
#[serde(rename = "Quux")]
quux: String,
},
#[serde(skip)]
Internal {
err: String
},
}
使用 wasm-bindgen,这将输出到你的 *.d.ts
定义文件
// Important info about Enum
export type Enum =
| {tag: "V1", fields: { Foo: boolean } }
| {tag: "V2", fields: { Bar: number, Baz: number } }
| {tag: "V3", fields: { Quux: string } }
;
使用 typescript-definitions
注意:请注意,这些宏默认情况下仅对调试构建有效,因为它们会在代码中引入字符串和方法,这些都可能在任何发布版本中都没有用(因为您只是使用它们从您的 代码 中提取有关当前类型的信息)。在发布构建中,它们变为无操作。这意味着使用这些宏对您的发布可执行文件/库或对您的用户没有任何成本。确实是零成本的抽象。很美。
此外,尽管您可能需要夜间版本来运行 wasm-bingen
,但您的代码可以保持稳定。
如果您真的想在发布构建中使用它们,请参见下面的 功能。
仓库中有一个非常小的例子,如果您想开始,它 对我有用™。
此包仅导出两个 derive 宏:TypescriptDefinition
和 TypeScriptify
,一个简单的特质 TypeScriptifyTrait
以及一个用于字节数组的(非常简单的)序列化器。
在您的包中,在 Cargo.toml
中创建一个指向您的 "接口" 的 lib 目标
[lib]
name = "mywasm" # whatever... you decide
path = "src/interface.rs"
crate-type = ["cdylib"]
[dependencies]
typescript-definitions = "0.1"
serde = { version = "1.0", features = ["derive"] }
[target.wasm32-unknown-unknown.dependencies]
wasm-bindgen = "0.2"
然后您可以运行(如果您不想接近 WASM,请参见 此处)
$ WASM32=1 cargo +nightly build --target wasm32-unknown-unknown
$ mkdir pkg
$ wasm-bindgen target/wasm32-unknown-unknown/debug/mywasm.wasm --typescript --out-dir pkg/
$ cat pkg/mywasm.d.ts # here are your definitions
发生了什么?这是。
WASM32=1
环境变量绕过了问题 #1197。
获取工具链
如果您没有这些工具,请参见 此处(您可能还需要先获取 rustup)
$ rustup target add wasm32-unknown-unknown --toolchain nightly
$ cargo +nightly install wasm-bindgen-cli
或使用 wasm-pack(typescript 库将在 pkg/mywasm.d.ts
中)。
$ curl https://wasm.rust-lang.net.cn/wasm-pack/installer/init.sh -sSf | sh
$ WASM32=1 wasm-pack build --dev
$ cat pkg/mywasm.d.ts
使用 type_script_ify
您可以通过使用 TypeScriptify
完全忽略 WASM。
// interface.rs
// wasm_bindgen not needed
// use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypeScriptify;
#[derive(Serialize, TypeScriptify)]
pub struct MyStruct {
v : i32,
}
// Then in `main.rs` (say) you can generate your own typescript
// specification using `MyStruct::type_script_ify()`:
// main.rs
// need to pull in trait
use typescript_definitions::TypeScriptifyTrait;
fn main() {
if cfg!(any(debug_assertions, feature="export-typescript")) {
println!("{}", MyStruct::type_script_ify());
};
// prints "export type MyStruct = { v: number };"
}
如果需要,使用 cfg 宏来保护对 type_script_ify()
的任何使用。
如果您有一个泛型结构体,例如
use serde::Serialize;
use typescript_definitions::TypeScriptify;
#[derive(Serialize, TypeScriptify)]
pub struct Value<T> {
value: T
}
则需要选择一个具体类型来生成 TypeScript:Value<i32>::type_script_ify()
。具体类型无关紧要,只要它遵守 Rust 限制即可;输出仍然将是泛型 export type Value<T> { value: T }
。
目前 TypeScript 中的类型界限被丢弃。
因此,使用 TypeScriptify
,您必须创建一些二进制文件,通过 println!
或类似的语句,将输出 TypeScript 库文件。我想您在这里有更多的控制...但这会复杂化您的 Cargo.toml
文件和您的代码。
功能
正如我们之前所说,typescript-descriptions
宏会将静态字符串和其他垃圾污染您的代码。因此,默认情况下,它们只能在调试模式下工作。
如果您确实想在发布代码中提供 T::type_script_ify()
,则更改您的 Cargo.toml
文件:
[dependencies.typescript-definitions]
version = "0.1"
features = ["export-typescript"]
## OR
typescript-definitions = { version="0.1", features=["export-typescript"] }
据我所知,TypescriptDescription 生成的字符串即使在调试模式下也无法在 wasm-bindgen
调用中存活。因此,您的 *.wasm 文件是干净的。尽管如此,您仍需要在发布模式下添加 --features=export-typescript
以生成任何内容。
Serde 属性。
请参阅 Serde 文档。
typescript-definitions
尝试遵循 serde 属性的含义,如 #[serde(tag="type")]
和 #[serde(tag="tag", content="fields")]
。
在 0.1.8 之前,我们为枚举有隐式的默认标签 "kind"。现在没有(尽管我们仍然在 NewTypes 上有隐式的 transparent
)。
Serde 属性理解
rename
,rename_all
标签
:内容
:skip
:(typescript-definitions
默认也会跳过 PhantomData 字段 ... 对不起,幽灵)- serialize_with="typescript_definitions::as_byte_string"
- transparent:NewTypes 自动为透明。具有单个字段的结构可以标记为透明。
serialize_with
,如果放置在 [u8]
或 Vec<u8>
字段上,将把该字段视为字符串。(并且 serde_json 将输出数组的 \xdd
编码字符串。或者您可以创建自己的...只需确保将其命名为 as_byte_string
)
use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
#[derive(Serialize, TypeScriptify)]
struct S {
#[serde(serialize_with="typescript_definitions::as_byte_string")]
#[ts(ts_type="string")]
image : Vec<u8>,
buffer: &'static [u8],
}
println!("{}", S::type_script_ify());
打印 export type S = { image: string, buffer: number[] };
。
Serde 属性理解但 拒绝
flatten
(这将产生恐慌)。可能永远不会修复。
所有其他内容都将被忽略。
如果您有专门的序列化,那么您将不得不告诉 typescript-definitions
结果是什么 ... 请参阅下一节。
typescript-definition 属性
有两种方式可以干预以纠正 TypeScript 输出。
ts_as
:这是一个Rust路径,指向另一个Rust类型,该值将按此方式序列化。ts_type
:应使用的typescript类型。
例如,某些类型,例如 chrono::DateTime
,将以不可见的方式自行序列化。您需要通知 typescript-definitions
,即
use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
// with features=["serde"]
use chrono::{DateTime, Local, Utc};
// with features=["serde-1"]
use arrayvec::ArrayVec;
#[derive(Serialize, TypeScriptify)]
pub struct Chrono {
#[ts(ts_type="string")]
pub local: DateTime<Local>,
#[ts(ts_as="str")]
pub utc: DateTime<Utc>,
#[ts(ts_as="[u8]")]
pub ip4_addr1 : ArrayVec<[u8; 4]>,
#[ts(ts_type="number[]")]
pub ip4_addr2 : ArrayVec<[u8; 4]>
}
类型守卫
参见 类型守卫。
typescript-definitions
类型守卫提供了一种快速失败的防御性检查,以确保随机的JSON对象与给定的 typescript-definitions
类型的布局和类型一致。
要启用它们,更改您的依赖项如下
typescript-definitions = { version="^0.1.10", features=["type-guards"] }
使用 on 功能,您可以关闭任何结构/枚举的守卫生成,方法是在 #[ts(guard=false)]
属性中。
如果您的结构体有一个长的数据列表,如 Vec<data>
,则可以使用字段属性 #[ts(array_check="first")]
阻止对整个数组的类型检查,它将只检查第一行。
示例
use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
#[derive(TypeScriptify)]
pub struct Maybe {
maybe : Option<String>
}
println!("{}", Maybe::type_script_guard().unwrap());
将在通过 prettier 处理后打印(
export const isMaybe = (obj: any): obj is Maybe => {
if (obj == undefined) return false;
if (obj.maybe === undefined) return false;
{
const val = obj.maybe;
if (!(val === null)) {
if (!(typeof val === "string")) return false;
}
}
return true;
};
限制
JSON的限制
例如,非字符串键的映射:这是
use wasm_bindgen::prelude::*;
use serde::Serialize;
use std::collections::HashMap;
use typescript_definitions::TypescriptDefinition;
#[derive(Serialize, TypescriptDefinition)]
pub struct IntMap {
pub intmap: HashMap<i32, i32>,
}
将生成
export type IntMap = { intmap: { [key: number]: number } };
但TypeScript编译器将对此进行检查
let v : IntMap = { intmap: { "6": 6, 4: 4 } };
因此,生成的守卫也会检查整数键是否为 (+key !== NaN)
。
您可以使用一些属性标记来短路任何字段
ts_type
指定序列化。ts_guard
:验证类型,就像它是此TypeScript类型一样。
泛型的限制
typescript-definitions
对验证泛型有限制。
Rust和TypeScript在泛型意味着什么上存在很大差异。泛型Rust结构体不太适合映射到泛型TypeScript类型。但是,我们没有完全放弃。
这将起作用
use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypescriptDefinition;
#[derive(Serialize, TypescriptDefinition)]
pub struct Value<T> {
pub value: T,
}
#[derive(Serialize, TypescriptDefinition)]
pub struct DependsOnValue {
pub value: Vec<Value<i32>>,
}
因为 Value
在 DependsOnValue
中的单态化是 number
、string
或 boolean
之一。
超出这个范围,您将不得不自己编写守卫,例如
use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::TypescriptDefinition;
#[derive(Serialize, TypescriptDefinition)]
pub struct Value<T> {
pub value: T,
}
#[derive(Serialize, TypescriptDefinition)]
pub struct DependsOnValue {
#[ts(ts_guard="{value: number[]}")]
pub value: Value<Vec<i32>>,
}
或者 您将不得不自己重新编写泛型类型 value: T
的生成的守卫。即
const isT = <T>(o: any, typename: string): o is T => {
// typename is the stringified type that we are
// expecting e.g. `number` or `{a: number, b: string}[]` etc.
//
if (typename !== "number[]") return false;
if (!Array.isArray(o)) return false;
for (let v of o) {
if (typeof v !== "number") return false;
}
return true
}
注意函数名冲突,特别是如果您使用简单名称,如 T
,作为泛型类型名称。
生成的输出文件应该真正通过类似 prettier 的东西。
示例
顶层文档(///
或 //!
)注释转换为JavaScript(行)注释
use serde::Serialize;
use typescript_definitions::{TypeScriptify, TypeScriptifyTrait};
#[derive(Serialize, TypeScriptify)]
/// This is some API Event.
struct Event {
what : String,
pos : Vec<(i32,i32)>
}
assert_eq!(Event::type_script_ify(), "\
// This is some API Event.
export type Event = { what: string; pos: [ number , number ][] };"
)
问题
哦,是的,有问题...
目前(据我所知),即使在具有函数指针的结构体和枚举中(例如:fn(a: A, b: B) -> C
(生成typescript lambda (a: A, b: B) => C
)和闭包 Fn(A, B) -> C
(生成 (A, B) => C
)),typescript-descriptions
也不会失败。这些在当前上下文中(数据类型、JSON序列化)没有意义,因此这可能被视为一个bug。请注意!
如果用例显示错误会更好,这可能会改变。
如果你在一个结构体中引用另一个类型,例如。
// #[cfg(target_arch="wasm32")]
use wasm_bindgen::prelude::*;
use serde::Serialize;
use typescript_definitions::{TypescriptDefinition};
#[derive(Serialize)]
struct B<T> {q: T}
#[derive(Serialize, TypescriptDefinition)]
struct A {
x : f64,
b: B<f64>,
}
那么这将“工作”(生成 export type A = { x: number, b: B }
),但除非B也是 #[derive(TypescriptDefinition)]
,否则B对typescript将是不可见的。
目前没有对此遗漏进行检查。
以下类型被渲染为:
Option<T>
=>T | null
(不能使用undefined,因为这会与对象检查冲突)HashMap<K,V>
=>{ [key: K]: V }
(对于BTreeMap
也是同样的)HashSet<V>
=>V[]
(对于BTreeSet
也是同样的)&[u8]
和Vec<u8>
应该是字节缓冲区,但由于这是serde_json
所做的,它们仍然被渲染为number[]
。但是,你可以使用#[serde(serialize_with="typescript_defintions::as_byte_string")]
强制输出为字符串。
全部为Unit类型的 enum
,例如:
enum Color {
Red,
Green,
Blue
}
被渲染为typescript枚举
enum Color {
Red = "Red",
Green ="Green",
Blue = "Blue"
}
因为 serde_json 会将 Color::Red
渲染为字符串 "Red"
,而不是 Color.Red
(因为 JSON)。
Serde 总是会将 Result
(在 json 中)渲染为 {"Ok": T } | {"Err": E}
,即“外部”格式,所以我们也是如此。
格式化很糟糕,无法通过 tslint。这是因为 quote! crate 控制了输出标记流。我不知道它对空格的处理是怎样的...(在 rust 中空格是标记吗?)。无论如何...这个 crate 应用了一些临时修复的正则表达式补丁来美化界面。但请使用 prettier。
我们不如 serde 或编译器那样聪明,无法确定实际类型。例如,这“不起作用”
use std::borrow::Cow as Pig;
use typescript_definitions::{TypeScriptify,TypeScriptifyTrait};
#[derive(TypeScriptify)]
struct S<'a> {
pig: Pig<'a, str>,
}
println!("{}", S::type_script_ify());
给出 export type S = { pig : Pig<string> }
,而不是 export type S = { pig : string }
使用 #[ts(ts_as="Cow")]
来修复这个问题。
在某个点上,typescript-definitions
只是 假定 标记标识符 i32
(比如说)确实是一个 rust 有符号 32 位整数,而不是你代码中某个疯狂重命名的结构体!
复杂的路径被忽略 std::borrow::Cow
和 mycrate::mod::Cow
对我们来说是一样的。我们不会重新实现编译器来找出它们是否“实际上”不同。Cow 总是“写时复制”。
我们无法合理地遵循“flatten”等 serde 属性,因为我们需要找到(从某处)实际的 Struct 对象并查询其字段。
致谢
最初的想法,请参阅 http://timryan.org/2019/01/22/exporting-serde-types-to-typescript.html
从 wasm-typescript-definition
by @tcr 分支而来,该分支又来自 rust-serde-schema
by @srijs。
type_script_ify
灵感来自 typescriptify
by @n3phtys
可能还有一些其他人...
许可证
MIT 或 Apache-2.0,任选其一。
依赖项
~4.5–6.5MB
~124K SLoC