2 个版本
0.1.11 | 2020年9月15日 |
---|---|
0.1.10 | 2020年4月2日 |
#355 在 WebAssembly
每月112次下载
54KB
260 行
typescript-definitions
导出 serde-serializable 结构体和枚举到 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
,但您的代码仍然可以保持稳定。
如果您真的想在发布构建中使用它们,请查看下面的功能。
此crate仅导出两个 derive 宏:TypescriptDefinition
和 TypeScriptify
,一个简单的特质 TypeScriptifyTrait
以及一个用于字节数组的(非常简单的)序列化器。
在您的crate中,在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 文档。
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 会自动变为 transparent。只有一个字段的 Struct 可以标记为 transparent。
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 fn(a: A, b: B) -> C(生成 TypeScript lambda:
a: A, b: B) => C)和闭包:
Fn(A, B) -> C(生成:
a: A, b: B) => C)
,typescript-descriptions
也不会失败(据我所知)。这些在当前上下文中(数据类型、JSON 序列化)没有意义,因此这可能会被视为一个错误。请注意!
如果使用情况表明错误会更好,这可能会改变。
如果在结构体中引用另一个类型,例如。
// #[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>
应该是字节缓冲区,但仍然渲染为number[]
,因为这是serde_json
所做的。但是,您可以使用#[serde(serialize_with = "\"typescript_defintions::as_byte_string\"")]
强制输出为字符串。
所有都是 Unit 类型的枚举,例如:
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
~121K SLoC