tsync icon indicating copy to clipboard operation
tsync copied to clipboard

Enhanced enum support and types

Open pendruct opened this issue 1 year ago • 9 comments

Hello again!

In my project, I use a lot of enums, and it is often desirable to match on the variants.

As a result, it'd be nice if something like this:

#[tsync]
struct AppleData {
  crunchy: bool
}

#[tsync]
struct BananaData {
  size: i32
}

#[tsync]
struct CarrotData {
  color: String
}

#[tsync]
enum Fruit {
  Apple(AppleData),
  Banana(BananaData),
  Carrot(CarrotData),
}

Converted to something like this:

export interface AppleData {
   crunchy: boolean
}

export interface BananaData {
  size: number
} 

export interface CarrotData {
  color: string
}

const enum FruitKinds {
  Apple,
  Banana,
  Carrot
}

export interface CaseFruitApple {
  kind: FruitKinds.Apple;
  data: AppleData;
}

export interface CaseFruitBanana {
  kind: FruitKinds.Banana;
  data: BananaData;
}

export interface CaseFruitCarrot {
  kind: FruitKinds.Carrot;
  data: CarrotData;
}

export type Fruit =
  | CaseFruitApple
  | CaseFruitBanana
  | CaseFruitCarrot;

My reasoning:

It provides the frontend code with everything it needs. Many functions may only be interested in the case for the banana, and can take just that as a parameter. It makes it so that none of the frontend types are anonymous anymore. It also provides an enum of all the variants as well, since that is often useful. And it allows for a full conversion of the ADT pattern, such that you can narrow the types in the frontend code. For example:

function renderApple(apple: AppleData) {
   // type-safe and narrowed to an apple
}

function renderBanana(apple: BananaData) {
   // type-safe and narrowed to a banana
}

function renderCarrot(apple: CarrotData) {
   // type-safe and narrowed to a carrot
}

switch (fruit.kind) {
  case FruitKinds.Apple: return renderApple(fruit.data);
  case FruitKinds.Banana: return renderBanana(fruit.data);
  case FruitKinds.Carrot: return renderCarrot(fruit.data);
}

Thoughts?

pendruct avatar May 12 '24 14:05 pendruct

just for reference, could you add what tsync currently does for the provided case?

AnthonyMichaelTDM avatar May 12 '24 23:05 AnthonyMichaelTDM

Oh, good idea! Here's the current output:

export interface AppleData {
  crunchy: boolean;
}

export interface BananaData {
  size: number;
}

export interface CarrotData {
  color: string;
}

export type Fruit =
  | { "Apple": AppleData }
  | { "Banana": BananaData }
  | { "Carrot": CarrotData };

The current "problems" with this are:

  • You can't type narrow. Because there's no discriminant like "kind", you can't do something like checking which variant it is and then having type-safe access to the internal data
  • It's an anonymous type, so for typescript functions that take a certain variant, you either have to manually create a wrapper type, or have a very long inline type (code duplication)
  • No const support (this probably won't affect everyone, but I only use const enums in my frontend code, because it maps from my data definitions and database in a more ergonomic way - also more performant)
  • Doesn't provide an enum of all the various discriminants, which can also be useful in frontend code

pendruct avatar May 12 '24 23:05 pendruct

Also, perhaps rather than changing the default behavior, this should just be another supported option? Because this would be a breaking change, in that enums would be converted in a different way. Basically, instead of inlining the variant data, it would wrap it in a { kind, data } sort of thing, to allow type narrowing and give each variant its own type

pendruct avatar May 12 '24 23:05 pendruct

Can you try using #[serde(flatten)], maybe that gives you something closer to what you're looking for..

Make sure you're using the latest version

AnthonyMichaelTDM avatar May 13 '24 18:05 AnthonyMichaelTDM

See https://github.com/Wulf/tsync/commit/2141be7fa51ee62ee8c6820a9389d7015dc6be78

AnthonyMichaelTDM avatar May 13 '24 18:05 AnthonyMichaelTDM

@AnthonyMichaelTDM What exactly am I supposed to be annotating with #[serde(flatten)]? Sorry, I am looking at the example now, and maybe I should be using #[serde(tag = "kind")] or something? That seems to theoretically get me to an output structure I'm moreso looking for (except, it's still stringly typed rather than using a const enum). Regardless, it doesn't seem to be working. Am I doing something wrong?

Cargo.toml:

[package]
name = "askjdhaskd"
version = "0.1.0"
edition = "2021"

[dependencies]
tsync = { git = "https://github.com/Wulf/tsync.git", branch = "main" }
serde_repr = "0.1"
serde = { version = "1.0", features = ["derive"] }

Input:

use std::path::PathBuf;
use tsync::tsync;
use serde::Serialize;

#[tsync]
#[derive(Serialize)]
struct AppleData {
  crunchy: bool
}

#[tsync]
#[derive(Serialize)]
struct BananaData {
  size: i32
}

#[tsync]
#[derive(Serialize)]
struct CarrotData {
  color: String
}

#[tsync]
#[derive(Serialize)]
#[serde(tag = "kind")]
enum Fruit {
  #[serde(rename = "apple")]
  Apple(AppleData),
  #[serde(rename = "banana")]
  Banana(BananaData),
  #[serde(rename = "carrot")]
  Carrot(CarrotData),
}

fn main() {
  let inputs = vec![PathBuf::from("./")];
  let output = PathBuf::from("../frontend/src/types/rust.ts");

  tsync::generate_typescript_defs(inputs, output, false, true);
}

Output:

/* This file is generated and managed by tsync */

export interface AppleData {
  crunchy: boolean;
}

export interface BananaData {
  size: number;
}

export interface CarrotData {
  color: string;
}

export type Fruit =;

Fruit just seems to have an empty type?

pendruct avatar May 14 '24 02:05 pendruct

huh... that's odd

AnthonyMichaelTDM avatar May 20 '24 04:05 AnthonyMichaelTDM

Fruit just seems to have an empty type?

Yeah, this just isn't implemented (see our tests for enum).

Here's the relevant section in the code: https://github.com/Wulf/tsync/blob/main/src/to_typescript/enums.rs#L226-L229

I pushed a fix that should help with the other cases though.

Regarding usage in the frontend though, I completely agree. If you don't have time to implement the above ^, you can try changing tuple enums to a struct enum with a field:

#[tsync]
enum Fruit {
  Carrot { data: CarrotData },
}

(I see your points about this here though)

That should hopefully get you a bit closer to what you want to achieve. Otherwise, I would love a PR! I think the original author (huge thanks to @AravindPrabhs :raised_hands: ) didn't need this case.

Wulf avatar Jun 12 '24 09:06 Wulf

@AnthonyMichaelTDM What exactly am I supposed to be annotating with #[serde(flatten)]? Sorry, I am looking at the example now, and maybe I should be using #[serde(tag = "kind")] or something? That seems to theoretically get me to an output structure I'm moreso looking for (except, it's still stringly typed rather than using a const enum). Regardless, it doesn't seem to be working. Am I doing something wrong? ...

After #59 is merged, #[serde(tag = "kind", content = "data"] would be the correct annotation

I'm not going to mark this as closed by 59, since that idea of discriminating by variants of another enum instead of using string literals is a good idea that might be worth implementing, separately.

AnthonyMichaelTDM avatar Feb 26 '25 03:02 AnthonyMichaelTDM