Is it possible to use a static value as a type? (like in Typescript)

I ask this question as a dev coming from Typescript, where something like this is possible when defining a type, and is particularly useful for discriminated unions:

enum ColorFormat {
  Hex,
  RGB,
}

type Color =
| { 
    format: ColorFormat.Hex, // <-- value is being used as a type here
    value: string,
  }
| { 
    format: ColorFormat.RGB, 
    r: number, 
    b: number, 
    g: number,
  };

Trying to do something like this in rust might look super similar:

enum ColorFormat {
    Hex,
    RGB,
}

enum Color {
    Hex { 
        format: "hex", // <-- static string
        value: String,
    },
    RGB { 
        format: ColorFormat::RGB, // <-- static usize (i think?) in this context
        r: u8, 
        b: u8, 
        g: u8,
    },
}

Of course, in the rust case, the format tag isn't exactly valuable because that information can be garnered by checking the enum variant itself. But, in my particular use case, I'm defining an enum for data that comes back from an API, and it'd be neat if I could specify the actual value of the discriminator field for each variant, rather than a broad String or numeric type.

EDIT:
Sorry, should have specified, I'm already using Serde for de/serialization purposes. What I'm interested in is having the type safety that comes with the compiler knowing in-advance what the value of something is. Not necessarily to this contrived example, but more broadly.

I would look into using serde with the tag attribute Link. serde is widely used in rust to handle serialization and deserialization of data types into/from all sorts of formats including JSON, xml and several others. Its also highly customizable, such as this type tag. It would look something like this.

Playground

use serde::{Serialize,Deserialize};

#[derive(Debug, Serialize,Deserialize)]
#[serde(tag = "format")]
enum Color {
    Hex { value: String },
    RGB { r: u8, g: u8, b: u8 }
}

fn main() {
    let colors: Vec<Color> = serde_json::from_str(r#"[
        { "format": "Hex", "value": "0x111111"},
        { "format": "RGB", "r": 17, "g": 17, "b": 17 }
    ]"#).expect("Failed to parse json");
    println!("{:?}",colors)
}

It effectively moves the problem of reading and writing the variant to just a serialization issue.

Ahh sorry, should have mentioned that I'm already using serde for my actual use case:

#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum Instrument {
    CashEquivalent {
        assetType: AssetType::CashEquivalent, <-- this is field whose value can be known in advance
        
        #[serde(flatten)]
        meta: InstrumentMeta,
    },
    ...
}

Added an edit to the question. Thanks for your response anyway!

Does every enum variant have assetType as a key? If so this would be better.

#[derive(Debug, Serialize)]
#[serde(tag="assetType")]
pub enum Instrument {
    #[serde(rename="CashEquivalentTypeName")]
    CashEquivalent {
        #[serde(flatten)]
        meta: InstrumentMeta,
    },
    ...
}

Alternatively if untagged works in your case, you should be able to just leave out assetType as a field and serde will just ignore it since it doesn't seem like you actually need it. Unless I'm misunderstanding your question.

No you're right. That's why I added the caveat to the question that in this case, assetType isn't actually valuable because I can just figure it out by matching an instrument. I'm just genuinely curious if this TS concept of value-as-type is a thing in rustworld, regardless of its usefulness. It's sounding like no tho?

I can't say I've seen it anywhere. Not that I've seen anywhere near all the rust code out there. But generally speaking, the Rust code i've seen tends to focus on only storing the data that is necessary to get the job done. If there is a value that is always the same for a given type, that would be handled at the serialization level with serde or it would just get extracted to an impl or trait such that you'd have a method with signature like

fn asset_type(instrument: &Instrument) -> AssetType {
    match instrument {
        Instrument::CashEquivalent{..} => AssetType::CashEquivalent
        ...
    }
}
1 Like

No, it doesn't exist in Rust.

I think this is where const generics are heading, but the details are still being worked out.

3 Likes

Another RFC that might be worth a look is enum_variant_types. It's not being actively worked on right now, but I hope that something like it will be released one day.

Consider enum variants types in their own rights. This allows them to be irrefutably matched upon. Where possible, type inference will infer variant types, but as variant types may always be treated as enum types this does not cause any issues with backwards-compatibility.

enum Either<A, B> { L(A), R(B) }

fn all_right<A, B>(b: B) -> Either<A, B>::R {
    Either::R(b)
}

let Either::R(b) = all_right::<(), _>(1729);
println!("b = {}", b);
2 Likes

You can achieve what you want with macros while we wait for const generics.

I do something like this to declare types that act like n-bit integers in bex (but each "bit" is an arbitrary boolean expression). I need to pass in the number of bits, the name for a standalone constructor function, they type name, and the primitive type it corresponds to u32, etc.

So, first I wrap the type declaration in macro_rules!:

https://github.com/tangentstorm/bex/blob/7434cac1e43879675c4e97c5c73d1a2efa7a70e8/src/int.rs#L140

And then whenever I want to declare a new type, I just call the macro:

https://github.com/tangentstorm/bex/blob/7434cac1e43879675c4e97c5c73d1a2efa7a70e8/src/int.rs#L241

In dynamic languages like TypeScript and Python a class name is itself a value (things which can exist in memory at runtime) that can be passed around at runtime. For example, in JavaScript it's typically a function that can be used with the new operator.

Rust doesn't let you do this because a type (constraints on how a value can be used) exists purely at compile time.

2 Likes

This gets the closest to what I had in mind. I really hope work on that RFC continues!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.