You can achieve the guarantees you're looking for, but not via string literals.
To get compile-time checks that the column type matches the column that you're asking for, you'd need to create marker types (the name of the column) with associated types (the type of the data). Something like this is feasible:
use std::any::Any; use std::collections::HashMap; trait Column { type Data: 'static; const NAME: &'static str; } struct Name; impl Column for Name { type Data = String; const NAME: &'static str = "name"; } struct Age; impl Column for Age { type Data = usize; const NAME: &'static str = "age"; } struct Table { data: HashMap<&'static str, Box<dyn Any>>, } impl Table { fn new() -> Table { Table { data: HashMap::new() } } fn set_column<C: Column>(&mut self, data: Vec<C::Data>) { self.data.insert(C::NAME, Box::new(data)); } fn get_column<C: Column>(&self) -> &Vec<C::Data> { self.data .get(C::NAME) .and_then(|data| data.downcast_ref::<Vec<C::Data>>()) .expect("table does not have that column") } } fn main() { let mut table = Table::new(); table.set_column::<Name>(vec!["Bob".to_owned(), "Alice".to_owned()]); table.set_column::<Age>(vec![12, 17]); dbg!(table.get_column::<Name>()); dbg!(table.get_column::<Age>()); }
[src/main.rs:50] table.get_column::<Name>() = [ "Bob", "Alice", ] [src/main.rs:51] table.get_column::<Age>() = [ 12, 17, ]
One flaw with this implementation is it doesn't guarantee at compile-time that the Table actually contains the column you're looking for. For that, you need to encode the column types into the table type like you suggested: Table<(Name, Age, ...)>. It also needs to allow compile-time lookup (does (Name, Age, ...) contain Age?) and the ability to extend the type ((Name,) + Age => (Name, Age)). This is a daunting bit of template juggling you'd have to handle, but there are crates that provide this kind of functionality.
Here's a working example using lhlist (not necessarily advocating for it, it was just a crate I found that works well enough for demonstration purposes). It has a similar API to what we have above and not only has the expressiveness we need, but also allows us to associate data with the individual column types:
#[macro_use] extern crate lhlist; use lhlist::{Label, LVCons, LookupElemByLabel, LabeledValue, Value, Nil}; new_label!(Name: Vec<String>); new_label!(Age: Vec<usize>); new_label!(Grade: Vec<usize>); struct Table<Columns> { columns: Columns } impl Table<Nil> { fn new() -> Table<Nil> { Table { columns: Nil::default() } } } impl<Columns> Table<Columns> { fn add_column<C>(self, data: C::AssocType) -> Table<LVCons<C, Columns>> where C: Label + 'static { Table { columns: lhlist::cons(lhlist::labeled_typearg::<C>(data), self.columns) } } fn get_column<C>(&self) -> &C::AssocType where C: Label + 'static, Columns: LookupElemByLabel<C, Elem = LabeledValue<C>>, { self.columns.elem().value_ref() } } fn main() { let table = Table::new(); let table = table.add_column::<Name>(vec!["Bob".to_owned(), "Alice".to_owned()]); let table = table.add_column::<Age>(vec![12, 17]); dbg!(table.get_column::<Name>()); dbg!(table.get_column::<Age>()); // dbg!(table.get_column::<Grade>()); // compile-time error }
[src\main.rs:42] table.get_column::<Name>() = [ "Bob", "Alice", ] [src\main.rs:43] table.get_column::<Age>() = [ 12, 17, ]
This could probably be made more ergonomic in a few regards, but I hope it shows how this could be done. Rust obviously does not have string literal types (I don't think anything can match the type-flexibility that Typescript has), but its not too much of a stretch to use more traditional struct types to achieve your goal.
polarscrate.