Affine types / ownership system
See original GitHub issueIt would be amazing if typescript could gain types with an ownership semantics as it could provide a very powerful tool against bugs caused by mutations. Idea is inspired by Rust’s owneship system and adapted to match ts / js language. Idea is to provide built-in types for which move semantics would be tracked. In the example below I’ll refer to that type as Own<t>
Ensures that there is exactly one binding to any given Own<t>
, if it is assigned to another binding type of the former binding should become Moved<t>
and type of the new binding should be Own<t>
.
const array:Own<number[]> = [1, 2, 3]
const array2 = array // => <Own<number[]>> [1, 2, 3]
const first = array[0] <- [TS]: `Moved<number[]>` has no index signature.
A similar thing should happens if function is passed Own<t>
, binding in the caller scope will get type Moved<t>
:
function take(array: Own<number[]>) {
// What happens here isn’t important.
}
let array:Own<number[]> = [1, 2, 3];
take(array);
array[0] // <- [TS]: `Moved<number[]>` has no index signature.
Rust also has also borrowing semantics which may be also worse pursuing. But for now I’m going to leave it out until I get some initial feedback.
Why
Typescript already provides tools to deal with the same class of issues using readonly
feature, which is nice but mostly useful with immutable data structures that do impose an overhead. With an ownership system it would be possible to author performance critical code without an overhead while still maintaining safety guarantees from the type system.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:22
- Comments:14 (3 by maintainers)
Top GitHub Comments
Affine/linear types would be extremely cool to have for state machines (e.g. chainable/fluent APIs where prior states cannot be reused), not just memory management/immutability/etc.
Productivity shouldn’t be an issue if it was opt-in (like in @Gozala’s example).
Yeah, I think this would be an amazing thing to see in TypeScript. It would even nudge me to favor TS over Flow for certain applications, because it would be better at eliminating a certain class of bugs.
If it helps, think of it as “Resource Management”, or “Resource Types” rather than the inscrutable “substructural.” Because the primary use is to model a fixed set of resources that can be shared among any number of owners. But in concrete terms, what they do is let you catch the following kinds of errors at compile time, rather than run-time (and hence eliminating the run-time overhead of a library implementation).
Because TypeScript objects don’t have destructors, and most types have reference semantics, it’s also hard to imagine pulling this off as a library. You’d have to implement it as a plugin, or additional tooling.
And any library solution would impose run-time overhead, and be limited to catching bugs at run-time. But as a language feature, something like
Unique<T>
could theoretically be type-erased back to T prior to code generation. And could even allow for additional optimizations down the road.It doesn’t necessarily have to be handled as types “changing” as I saw in some earlier comments. You could potentially provide a set of primitives which enable certain static guarantees.
singleton
- statically enforce (there are a few approaches I can think of) that only a single instance can ever exist. There are a few different approaches with different trade-offs. The easiest to explain is that a “singleton
” is just aclass
where all members are implicitly static, and the constructor runs exactly once. All references to somesingleton S
refer to the same object.move_only
- statically enforce that only one binding to some T can exist at a time. I.e. if you pass a move_only` to a function, you can’t use that binding again until you’ve assigned to it. But callees are allowed to return the T back to you, or pass it on.single_use
- statically enforce that some T is used at most once. There needs to some scheme to determine when a value is considered “consumed”.must_use
- Make sure the value gets consumed somewhere.Note that some combinations of these types make sense:
single_use
+must_use
compose to give you “use exactly once”. A type could theoretically besingle_use
without beingmove_only
. It makes perfect sense to have amove_only singleton
, or even amove_only must_use singleton
.Closures do complicate things, but I suspect there conservative solutions that won’t break the language while still providing some utility.