#typescript #node #javascript #node-bindgen #snake-case

tslink

根据Rust代码创建TypeScript定义

4个版本

0.1.3 2024年4月16日
0.1.2 2024年1月13日
0.1.1 2024年1月13日
0.1.0 2023年11月21日

过程宏 中排名第 1364

每月下载量 21

Apache-2.0

160KB
3.5K SLoC

LICENSE Crates.io

tslink

tslink 将Rust类型表示为 TypeScript 类型。它帮助创建包含所有必要定义和类型的npm包(基于node模块)。

目录

  1. 它如何有用
  2. 构建
  3. 输出
  4. 结构体
  5. 枚举
  6. 使用方法
  1. 配置文件
  2. QA和故障排除

它如何有用?

Node模块

如果您正在开发基于Rust的Node模块,例如使用 node-bindgen crate,tslink 将生成一个包含所有必要TypeScript定义的npm包。这有助于Node模块的集成和测试。

共享类型

如果您正在开发例如基于Rust的服务器部分,并且客户端部分在TypeScript中,您可能对将一些Rust类型共享到TypeScript世界感兴趣。请求数据或响应可以表示为 *.ts 文件中的TypeScript定义。

构建

由于tslink生成工件,默认情况下,tslink侧的任何I/O操作都会被跳过。这是因为编译可以由多个原因触发(clippy、rust analyzer等),并且在同一文件范围内会产生不可预测的I/O操作和I/O错误。要允许tslink生成工件,应使用具有任何正值的环境变量 TSLINK_BUILDtrue1on)。

export TSLINK_BUILD=true && cargo build

注意:tslink仅在JavaScript和TypeScript中创建未来节点模块的表示。要创建本地节点模块,可以使用node-bindgen

输出

基于Rust代码tslink生成

  • JavaScript(*.js)用于npm包(库)
  • 类型定义文件(*.d.ts
  • 可选的TypeScript文件(*.ts)与接口

例如,对于npm包tslink,它会生成

- destination_path
    - lib.d.ts     # TypeScript definition
    - lib.js       # Javascript module representation
    - package.json # NPM package description

可选地,tslink可以生成*.ts文件。这些文件不是npm包的一部分,仅用于在Rust和TypeScript之间“共享”类型。一旦*.ts文件不是npm包的一部分,应该单独定义其目标路径。

# #[macro_use] extern crate tslink;
# use tslink::tslink;
#[tslink(target = "./target/selftests/interfaces/interfaces.ts")]
struct TestingA {
    pub p8: u8,
    pub p16: u16,
    pub p32: u32,
    pub p64: u64,
    pub a64: u64,
}

将生成./target/selftests/interfaces/interfaces.ts,包含

export interface TestingA {
    p8: number;
    p16: number;
    p32: number;
    p64: number;
    a64: number;
}

结构体

tslint默认将结构体表示为接口,但也可以表示为class。如果结构体具有一些方法且这些方法被传播到节点模块中,应使用类表示。

如果将结构体用作类型定义,最好使用接口表示。

对于下一个Rust代码tslink生成*.js*.d.ts文件。

# #[macro_use] extern crate tslink;
# use tslink::tslink;
# use std::collections::HashMap;
#[tslink(class)]
struct StructureA {
    pub p8: u8,
    pub p16: u16,
    pub p32: u32,
    pub p64: u64,
    pub a: (u32, u64),
    pub b: Vec<u64>,
    pub c: HashMap<String, u64>,
    pub s: String,
    pub k: Option<String>,
}

#[tslink]
impl StructureA {
    #[tslink]
    pub fn method_a(&self, abs: u8) -> u8 {
        0
    }
}

TypeScript类型定义(*.d.ts)表示

export declare class StructureA {
    p8: number;
    p16: number;
    p32: number;
    p64: number;
    a: [number, number];
    b: number[];
    c: { [key: string]: number };
    s: string;
    k: string | null;
    method_a(abs: number): number;
}

枚举

扁平枚举将表示为经典的TypeScript枚举

# #[macro_use] extern crate tslink;
# use tslink::tslink;
#[tslink]
enum FlatEnum {
    One,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Nine,
}

*.d.ts

export enum FlatEnum {
    One,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Nine,
}

但任何具有嵌套类型的枚举在TypeScript侧将表示为interface

# #[macro_use] extern crate tslink;
# use tslink::tslink;
#[tslink]
enum SomeEnum {
    One,
    Two,
    Three(u8),
    Four(u8, u16, u32),
    Five((String, String)),
    Six(Vec<u8>),
}

*.d.ts

export interface SomeEnum {
    One?: null;
    Two?: null;
    Three?: number;
    Four?: [number, number, number];
    Five?: [string, string];
    Six?: number[];
}

使用方法

属性

属性 使用方法 描述 应用
#[tslink()] 告诉tslink创建TypeScript类而不是interface 结构体
忽略 #[tslink(忽略)] 忽略当前结构体的字段或方法 结构体方法
忽略= "列表" #[tslink(忽略= "field_a; field_b; method_a")] 要忽略的字段/方法的列表。只能在结构体声明上定义。 结构体
snake_case_naming #[tslink(snake_case_naming)] 将结构体的字段或方法重命名为snake_case命名(my_field_a变为myFieldA 结构体方法,函数
重命名= "名称" #[tslink(重命名= "新的字段或方法名称")] 将结构体的方法或函数重命名为给定的名称 结构体方法和函数
构造函数 #[tslink(构造函数)] 将当前方法标记为构造函数。实际上,只能为返回Self的方法定义。 结构体方法返回Self
目标= "路径" #[tslink(目标= "./path_to/file.ts")] 告诉tslink将TypeScript定义*.ts / *.d.ts保存到指定的文件 结构体,枚举
异常抑制 #[tslink(异常抑制)] 默认情况下,如果出错,方法/函数会抛出JavaScript异常。如果使用“异常抑制”,方法/函数将返回JavaScript Error而不是抛出异常 结构体方法,函数
结果= "json" #[tslink(结果= "json")] Ok 情况转换为 Result<T, _> 的 JSON 格式 结构体方法,函数
错误= "json" #[tslink(错误= "json")] Err 情况转换为 Result<_, E> 的 JSON 格式 结构体方法,函数
fn_arg_name= "ref_to_entity" #[tslink(data= "MyStruct")] 在 Rust 端将参数类型与结构体/类型/枚举绑定 结构体方法,函数

多个属性

可以定义多个属性

# #[macro_use] extern crate tslink;
# use tslink::tslink;
#[tslink(
    class,
    target = "./target/selftests/interfaces/interfaces.ts; ./target/selftests/interfaces/interfaces.d.ts",
    ignore = "_p8;_p16;_p32"
)]
struct MyStruct {
    pub _p8: u8,
    pub _p16: u16,
    pub _p32: u32,
    pub _p64: u64,
    pub a64: u64,
}

impl MyStruct {
    #[tslink(snake_case_naming, exception_suppression)]
    fn my_method(&self) -> Result<i32, String> {
        Err("test".to_string())
    }
}

#[tslink(snake_case_naming, exception_suppression)]
fn my_function() -> Result<i32, String> {
    Err("test".to_string())
}

结构体到TypeScript类

要反射 struct 到 TypeScript 类 #[tslink(class)],因为默认情况下 tslink 将 struct 表示为 interface

如果结构体有特定的构造函数,则应使用 #[tslink(constructor)] 标记该方法。

# #[macro_use] extern crate tslink;
# use tslink::tslink;
#[tslink(class)]
struct MyStruct {
    pub field_a: u8,
}

impl MyStruct {
    #[tslink(constructor)]
    fn new() -> Self {
        Self { field_a: 0 }
    }
    #[tslink]
    fn my_method(&self) -> Result<i32, String> {
        Err("test".to_string())
    }
}

如果 struct 没有字段 #[tslink(class)] 可以直接应用于 impl

# #[macro_use] extern crate tslink;
# use tslink::tslink;

struct MyStruct { }

#[tslink(class)]
impl MyStruct {
    #[tslink(constructor)]
    fn new() -> Self {
        Self { }
    }
}

注意:如果您的结构体有构造函数,则必须使用 #[tslink(constructor)] 标记该方法,以便 tslink 在 JS 反射中表示构造函数。

结构体/枚举到TypeScript接口

要反射 structenum 到 TypeScript interface,应使用 #[tslink]

# #[macro_use] extern crate tslink;
# use tslink::tslink;
#[tslink]
struct MyStruct {
    pub field_a: u8,
    pub field_b: u8,
    pub field_c: u8,
}

#[tslink]
enum MyFlatEnum {
    One,
    Two,
    Three,
}

#[tslink]
enum MyEnum {
    One(String),
    Two(i32, i32),
    Three,
}

注意,“扁平”枚举(MyFlatEnum)将转换为经典的 TypeScript enum,但复合枚举(MyEnum)将转换为 interface

异步方法/函数

异步方法/函数的结果将在 TypeScript 端表示为 Promise

# #[macro_use] extern crate tslink;
# use tslink::tslink;
struct MyStruct {
}

#[tslink(class)]
impl MyStruct {

    #[tslink]
    async fn my_async_method(&self) -> i32 {
        0
    }
}

将表示为

export declare class MyStruct {
    my_async_method(): Promise<number>;
}

注意:使用 promises 抑制 JS 异常没有意义,使用此属性将不会产生影响。

方法/函数中的回调

定义回调的最佳方式是使用泛型类型。

# #[macro_use] extern crate tslink;
# use tslink::tslink;
struct MyStruct {}

#[tslink(class)]
impl MyStruct {
    #[tslink]
    fn test_a<F: Fn(i32, i32)>(&self, callback: F) {
        callback(1, 2);
    }
}

将表示为

export declare class MyStruct {
    testA(callback: (arg0: number, arg1: number) => void): void;
}

命名方法/字段

TypeScript/JavaScript 命名标准:snake_case 命名。一些 crate,如 node-bindgen,将根据此规则自动重命名字段和方法。为了适应这种行为,tslink 应该知道哪些字段/方法应该被重命名。

最简单的方法是在方法/字段级别使用 #[tslink(snake_case_naming)]。或者在非常具体的用例中,可以使用 #[tslink(rename = "newNameOfFieldOrMethod")] 来给方法/字段指定特定的名称。

# #[macro_use] extern crate tslink;
# use tslink::tslink;

#[tslink(class, snake_case_naming)]
struct MyStruct {
    field_a: i32,
}

#[tslink(class)]
impl MyStruct {
    #[tslink(snake_case_naming)]
    fn my_method_a(&self) -> i32 {
        0
    }
    #[tslink(rename = "newNameOfMethod")]
    fn my_method_b(&self) -> i32 {
        0
    }
}

将表示为

export declare class MyStruct {
    thisIsFieldA: number;
    myMethodA(): number;
    newNameOfMethod(): number;
}

注意#[tslink(rename = "CustomName")] 不能用于重命名字段,但可以将 snake_case_naming 应用到结构体上的字段。

绑定数据。参数绑定。

在 Rust 中,可以使用一些数据类型将方法/函数参数类型绑定到 #[tslink(data = "MyStruct")]

#[macro_use] extern crate tslink;
use serde::{Deserialize, Serialize};
use tslink::tslink;

// Define error type for bound method
#[tslink]
#[derive(Serialize, Deserialize)]
struct MyError {
    msg: String,
    code: usize,
}

// Make possible convert serde_json error into our error implementation
impl From<serde_json::Error> for MyError {
    fn from(value: serde_json::Error) -> Self {
        MyError {
            msg: value.to_string(),
            code: 1,
        }
    }
}

#[tslink]
#[derive(Serialize, Deserialize)]
struct MyData {
    pub a: i32,
    pub b: i32,
}

struct MyStruct { }

#[tslink(class)]
impl MyStruct {
    #[tslink(
        my_data = "MyData",
        error = "json",
    )]
    fn get_data(&self, my_data: String) -> Result<i32, MyError> {
        println!("my_data.a = {}", my_data.a);
        println!("my_data.b = {}", my_data.b);
        Ok(my_data.a + my_data.b)
    }}

将表示为

export declare class MyStruct {
    getData(my_data: MyData): number;
}

重要

  1. tslink 将绑定的数据转换为 JSON 字符串。它需要在您的项目中将 serdeserde_json 作为依赖项。
  2. 由于解析 JSON 字符串 可能会出错,因此方法/函数应仅返回 Result<T, E>
  3. 因为 serde_json 返回 serde_json::Error 错误类型应该可从 serde_json::Error 转换。
  4. 在大多数情况下,您会使用带有 #[tslink(error = "json")] 的数据绑定,因为它允许您使用自己的错误实现。这是一种推荐的方式。
  5. 在 Rust 端方法/函数的声明中,参数类型应为 String(例如:fn get_data(&self, my_data: String) -> Result<MyData, MyError>),但在方法/函数的主体中,此参数将被视为绑定的类型。
  6. 绑定的类型和错误应该实现 SerializeDeserialize

绑定数据。结果/错误绑定。

要绑定错误与您的自定义类型之一,应使用 #[tslink(error = "json")],就像在 "绑定数据. 参数绑定" 中显示的那样。例如,错误参数将序列化为 Rust 级别的 JSON 字符串,并在 TypeScript/JavaScript 级别解析 JSON 字符串

要将结果与自定义数据类型绑定,应使用以下代码:#[tslink(result = "json")]

#[macro_use] extern crate tslink;
use serde::{Deserialize, Serialize};
use tslink::tslink;

// Define error type for bound method
#[tslink]
#[derive(Serialize, Deserialize)]
struct MyError {
    msg: String,
    code: usize,
}

// Make possible convert serde_json error into our error implementation
impl From<serde_json::Error> for MyError {
    fn from(value: serde_json::Error) -> Self {
        MyError {
            msg: value.to_string(),
            code: 1,
        }
    }
}

#[tslink]
#[derive(Serialize, Deserialize)]
struct MyData {
    pub a: i32,
    pub b: i32,
    pub c: String,
}

struct MyStruct { }

#[tslink(class)]
impl MyStruct {
    #[tslink(
        my_data = "MyData",
        result = "json",
        error = "json",
    )]
    fn get_data(&self, my_data: String) -> Result<MyData, MyError> {
        Ok(MyData {
            a: my_data.a + 1,
            b: my_data.b + 1,
            c: format!("{}{}", my_data.c, my_data.c),
        })
    }}

将表示为

export declare class MyStruct {
    getData(my_data: MyData): MyData;
}

重要

  1. tslink 将绑定的数据转换为 JSON 字符串。它需要在您的项目中将 serdeserde_json 作为依赖项。
  2. 由于解析 JSON 字符串 可能会出错,因此方法/函数应仅返回 Result<T, E>
  3. 因为 serde_json 返回 serde_json::Error 错误类型应该可从 serde_json::Error 转换。
  4. 在大多数情况下,您会使用带有 #[tslink(error = "json")] 的数据绑定,因为它允许您使用自己的错误实现。这是一种推荐的方式。
  5. 在 Rust 端方法/函数的声明中,参数类型应为 String(例如:fn get_data(&self, my_data: String) -> Result<MyData, MyError>),但在方法/函数的主体中,此参数将被视为绑定的类型。
  6. 结果类型和错误实现应使用 SerializeDeserialize

异常抑制

抛出异常或没有异常取决于创建节点模块使用的库/crate。例如,node-bindgen 在方法/函数完成时遇到错误就会在JavaScript级别抛出异常。但tslink允许自定义此场景。

默认情况下,异常抑制是关闭的,Rust级别的任何错误都会在JavaScript级别成为异常。

让我们看看前面的示例。

# #[macro_use] extern crate tslink;
# use serde::{Deserialize, Serialize};
# use tslink::tslink;
# #[tslink]
# #[derive(Serialize, Deserialize)]
# struct MyError {
#     msg: String,
#     code: usize,
# }
# // Make possible convert serde_json error into our error implementation
# impl From<serde_json::Error> for MyError {
#     fn from(value: serde_json::Error) -> Self {
#         MyError {
#             msg: value.to_string(),
#             code: 1,
#         }
#     }
# }
struct MyStruct { }

#[tslink(class)]
impl MyStruct {
    #[tslink(
        error = "json",
    )]
    fn get_data(&self, my_data: String) -> Result<i32, MyError> {
        Err(MyError { msg: "Test".to_string(), code: 1})
    }}

将表示为

export declare class MyStruct {
    getData(my_data: MyData): number;
}

方法 getData 返回 MyData,但如果发生错误,将抛出JavaScript异常。

使用 #[tslink(exception_suppression)] 我们可以改变它。

# #[macro_use] extern crate tslink;
# use serde::{Deserialize, Serialize};
# use tslink::tslink;
# #[tslink]
# #[derive(Serialize, Deserialize)]
# struct MyError {
#     msg: String,
#     code: usize,
# }
# // Make possible convert serde_json error into our error implementation
# impl From<serde_json::Error> for MyError {
#     fn from(value: serde_json::Error) -> Self {
#         MyError {
#             msg: value.to_string(),
#             code: 1,
#         }
#     }
# }
struct MyStruct { }

#[tslink(class)]
impl MyStruct {
    #[tslink(
        error = "json",
        exception_suppression
    )]
    fn get_data(&self, my_data: String) -> Result<i32, MyError> {
        Err(MyError { msg: "Test".to_string(), code: 1})
    }}

将表示为

export declare class MyStruct {
    getData(my_data: MyData): number | (Error & { err?: MyError});
}

现在 getData 返回或 number,或错误时返回 Error & { err?: MyError},但异常被抑制。

是否使用此功能取决于开发者,但通常这是一种减少JavaScript/TypeScript侧的try/catch块并准备好可能发生错误的地方的好方法。

与node-bindgen一起使用

node-bindgen crate 允许创建原生节点模块,并通过tslink获得完整的npm项目。

通用用法中只有一个规则——调用 #[tslink] 应始终在调用 #[node_bindgen] 之前。

#[macro_use] extern crate tslink;
use tslink::tslink;
use node_bindgen::derive::node_bindgen;

struct MyScruct {}

#[tslink(class)]
#[node_bindgen]
impl MyScruct {
    #[tslink(constructor)]
    #[node_bindgen(constructor)]
    pub fn new() -> Self {
        Self {}
    }

    #[tslink(snake_case_naming)]
    #[node_bindgen]
    fn inc_my_number(&self, a: i32) -> i32 {
        a + 1
    }
}

请注意node-bindgen 默认将方法名称转换为蛇形命名。您应使用 #[tslink(snake_case_naming)] 来考虑这一点。

默认情况下,node-bindgenroot./dist 文件夹中创建 index.node。在 Cargo.toml 文件中应在 [tslink] 节中定义适当的路径。

文件:./Cargo.toml(项目根目录)

[project]
...

[lib]
...

[tslink]
node = "./dist/index.node"

[dependencies]
...

有关 node-bindgen 的完整示例,请参阅此处。要启动它

git clone https://github.com/icsmw/tslink.git
cd tslink/examples/node_bindgen
sh ./run_test.sh

配置

tslink的全局配置可以在项目根目录下的Cargo.toml文件的[tslink]部分定义。在大多数情况下,这是必需的。此设置允许定义一个指向本地Node模块的路径,该模块将与npm包绑定。

但如果tslink仅用于在*.ts文件中生成接口,则可以跳过配置文件。

./Cargo.toml中的tslink设置示例

[package]
name = "tslink-test"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]
path = "rs/lib.rs"

[tslink]
# [required] path to native node module
node = "./dist/index.node"
# [optional] global rule of renaming (can be: "method" or "fields" or both - "methods,fields")
snake_case_naming = "methods"
# [optional] global rule for javascript exception suppression
exception_suppression = true
字段 必需 描述
node= "path_to_native_node_module" yes 文件路径 本地Node模块路径
snake_case_naming= "规则" "methods", "fields"或"methods,fields" 全局重命名规则
异常抑制= true 布尔值 全局JavaScript异常抑制规则

QA和故障排除

问题:tslink没有创建任何文件

答案:确保环境变量TSLINK_BUILD已导出,值为true1


问题:rust-analyzer报告tslink的IO错误

答案:删除环境变量TSLINK_BUILD或将它设置为false0


问题:什么是./target/selftests

答案:这些是tslink使用cargo test创建的工件。可以安全删除。


问题:tslink创建本地Node模块(如index.node)吗?

问题:不,tslink仅创建未来Node模块在JavaScript和TypeScript中的表示。要创建本地Node模块,可以使用crate node-bindgen


问题:使用node-bindgen时,在JavaScript端出现错误,例如"no method_call_b() on undefined"。

问题:注意,node-bindgen默认将方法名应用为蛇形命名。您应该使用#[tslink(snake_case_naming)]来考虑这一点。

依赖项

~1.3–2MB
~39K SLoC