typescriptrustownershiplinear-types

Represent a consumable, single-use object in TypeScript


In Rust, a struct can have a method that takes ownership of the struct and moves it, making it illegal to use the struct again later, e.g.

struct Foo {}

impl Foo {
  fn consume(self) -> u32 {
    println!("consumed!");
    42
  }
}

fn main() {
  let x = Foo {};
  let val_1 = x.consume();
  let val_2 = x.consume(); // WON'T COMPILE
}

Is there any way to represent a similar state change using TypeScript's type system?


Solution

  • TypeScript does not currently support substructural types like linear/affine types, which are needed to represent the sort of ownership model you're trying to use here. There's a longstanding open feature request for it at microsoft/TypeScript#16148 listed as "awaiting more feedback", meaning they need to hear more community demand for it before they'd seriously consider implementing it. And there doesn't seem to be enough of that. It wouldn't hurt for you to add your 👍 and describe your use case if it's compelling, but it probably wouldn't help either.


    TypeScript doesn't really model arbitrary state changes in its type system. It only really allows for narrowing, meaning that at best you could do something to x which would make TypeScript treat it as more specific type. Generally speaking more specific types have more capabilities than less specific types, so on first blush this isn't really something you can do easily. You could sort of, kind of, use assertion functions to narrow x.consume all the way to the never type, which will give you an error if you call it. But assertion functions have caveats: they can't return a value (so assigning to val_1 wouldn't do much) and you need an explicit type annotation. So the closest I can get to what you want looks like this:

    class Foo {
        consume(
            ret: { value: number; }
        ): asserts this is { consume: never } {
            console.log("consumed!")
            ret.value = 42;
        }
    }
    
    const foo: Foo = new Foo();
    const ret = { value: 0 };
    foo.consume(ret);
    let val1 = ret.value;
    //  ^?
    console.log(val1); // 42
    foo.consume(ret);
    //  ~~~~~~~
    // error! This expression is not callable.
    

    Here a Foo instance has a consume() assertion method which accepts a ret parameter which acts as a container for the desired return value. If you call consume() on an instance of Foo (which has been explicitly annotated as Foo), then it will mutate ret and the narrow the instance so that consume is of type never and it cannot be called again.

    Whether or not this is actually better than just giving up and representing the state in some other way (e.g., just have consume() be callable multiple times but return a cached value after the first time, etc.) depends on your use case. I'd doubt assertion methods are the way to go, but that's outside the scope of the question, really.

    Playground link to code