Repository navigation
Affine types / ownership system #16148
Description
It 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.
Activity
HerringtonDarkholme commentedon May 31, 2017
Hi Irakli Gozalishvili (@Gozala). Welcome to TypeScript.
While ownership system and linear type are interesting language feature, I recommend you to read Design Goal of TypeScript. Item 1,3,7 are related here.
Linear Type system seems to
Exactly mimic the design of existing languages, alas, Rust.Also it seems to require a more "correct" type system, so correct that it harms productivity for ordinary JavaScript/TypeScript developers who never hear of ownership/RAII.
Own<T>in a garbage collected language acts no more than a stricterObject.freeze, though.IMHO any programmers other than Rust and C++ will be surprised at why a variable suddenly becomes unavailable after use. TypeScript shouldn't require users to grok
std::unique_ptr<T>before using.TypeScript has already sacrificed safety for productivity, for example, bivariance. I wonder if linear type system would ever on TS' roadmap.
Gozala commentedon May 31, 2017
Thanks!
I did read them and to be honest I don't see any conflict here. To be specific:
That is primary goal of this proposal & specifically it would help statically identify instances where mutation may take place without a concessus - explicitly expresing which part of code is responsible for state changes at specific code paths.
I don't really know why would you imagine runtime overhead here. If anything linear types provide a lot of the same guarantees as immutability but without the memory overhead that immutability imposes.
Again not sure why would you assume that. Mostly linear types just ensure that there's a single "actor" (in a broad sense not to confuse with an actor model) performing mutation.
I really don't see analogy with freeze here. It's by no means becomes immutable. I do repeat myself, but only thing type checker will be doing different is ensuring that once reference to binding is passed that binding is no longer used (naively assuming type of that binding can be marked as moved so any usage will be an error).
I don't think that's what is being proposed. Also I would assume if you do use something like
Own<t>you would read a docs for it to understand what does it provide.To be clear by no means I am proposing to make ownership system a default behavior for all bindings, that would indeed be absurd.
Instead I am proposing adding a built-in type similar to ReadonlyArray that can provide safety against data races in performance critical code where immutability is unacceptable option due to mentioned overhead
Now whether ownership system is bringing enough value to the ts audience to justify an effort or if it is even compatible with a current design is a different matter, also a reason I opened this issue to find that out.
Edit: fixed some typos
HerringtonDarkholme commentedon May 31, 2017
I mean the item 1, 3, 7 in non-goal section.
Rust
Own<T>is not provably correct. But certainly a lot of typing complexity is added. Just consider how assignability rule should apply to aTandOwn<T>andMove<T>, and how it applies to flow sensitive typing, structural typing and union type/intersection type.Why a variable of
Own<T>will be changed toMove<T>. Can you provide a language example that a variable type is changed after variable usage, other than Rust? TS does have flow sensitive typing where variable type is refined after predication likeinstanceofbut using variable as argument has nothing to do with narrowing type.Gozala commentedon May 31, 2017
It's not specific to Rust it's just substructural typing which may not be as mainstream as structural typing which is what TS is, and in that case all of the TS does mimic other languages of that family.
Those are all good questions and answers may be simple or complex depending what direction design is taken. That being said I'd rather not get into weeds here, until there is some initiative to implement this.
I really don't think explaining semantics of linear types & it's derivations or languages that implement them makes much sense here. There are better resources available for this I'd suggest to star with Substructural type system wiki page, also worth noting that provided language list is far from complete.
That being said, I do agree that substructural types are not quite as mainstream yet & Rust and Idris are probably two names that stand out. That does imply:
As of "likely to surprise", well I'd argue typescript does have enough of that even for users coming from structural type systems that typescript seems to provide. So where line is drawn I do not know, but I think provided utility play a role in determining how much of surprise factor is acceptable.
I'm not sure what is the point you're trying to make here. Sure TS does not do this (should I say yet 😄), but hey I we would not be having this conversation if it did. I also in no way tried to imply that adding this feature will be free in terms of design or implementation work. In fact my main concern was that effort it would require may very well out-weight the benefit it's going to provide. All I can tell I'd be happy to help this effort if it happens.
P.S.: Don't take it a wrong way please, but most of your commentary seems to be questioning design of linear type system itself and some assumptions indicate you may be misunderstanding a big picture. I'd really encourage you to look more into this subject, you may just find out that it fits JS better than structural types, it certainly does IMO, structural types work great in pure functional languages (that I really wish JS was more of), but with level of mutability and imperativeness you find in JS you end up loosing many of the benefits. Linear types on the other hand seem to allow all of that.
RyanCavanaugh commentedon May 31, 2017
It's important to interpret the non-goals in the right direction. What we mean is "You should have feature X because it's in language Y and Y is a good language" is a bad argument, and so is "You shouldn't do feature X because it is in language Y". Basically a feature should support itself in terms of value, not mimicry.
I'm not sure Rust would have owernship semantics if it weren't for their constraint of having a sound memory management model. It certainly has positive side effects on mutability analysis but that doesn't seem to be the primary objective.
There are a lot of other JavaScript patterns for handling the scenario of an object that has exactly one mutation site; closures come to mind as the most obvious solution. Without some extremely compelling use cases that would justify something as complex as this, I don't see it being a good fit for TypeScript.
interphx commentedon Jun 4, 2017
I think there is a use case for ownership system in performance-sensitive projects (games, simulations, heavy mobile apps). I've encountered the following issue while working with space partitioning data structures, creating multidimensional views of arrays, etc.
Consider these two classes:
Although the particular example is contrived, it demonstrates a pattern that I often see in performance-critical projects: maintaining an object and repopulating it instead of creating a new one every time.
The problem: it is impossible for the type system to indicate that the object the caller received may change and become invalid in the future. If the calling site wishes to preserve it, they need to explicitly call
dataProcessor.getFirstItems().slice(0), otherwise they can only process and discard it.Casting the returned object to an immutable interface does not solve this: it only prevents the calling site from modifying it.
While I doubt that a complete affine or linear type system is a valid goal for TypeScript, supporting cases like this would make performance-sensitive code a lot more safe.
Further issues I see:
tjpalmer commentedon Mar 27, 2019
Further, a basic linear type system doesn't have to be perfectly sound. Easy type assertions could circumvent for convenience, and that would be ok for TypeScript. I think the ability to express type changes on certain operations would be very handy.
emdash commentedon Sep 29, 2019
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 aclasswhere all members are implicitly static, and the constructor runs exactly once. All references to somesingleton Srefer 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_usecompose to give you "use exactly once". A type could theoretically besingle_usewithout 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.
dima-starosud commentedon Nov 25, 2019
TypeScript tracks when variable changes its type:
This can be used for modeling linear typing discipline etc.
elslooo commentedon Dec 6, 2019
I think this is a really interesting feature. For anyone who's familiar with Rust, its benefits are clear: there is an entire class of bugs that this will prevent (even apart from safe memory management, see @khoomeister). In the future, it might also yield new opportunities for optimizations.
I think we can also agree that ownership has a steep learning curve and probably won't be useful in the majority(?) of projects TS is typically used for (web frontend).
So: what would be needed to get ownership into TS in a pragmatic fashion? If I understand correctly, the borrow checker in Rust is a separate stage that comes after type checking etc. I think it is certainly viable to do the same with TS: insert a new stage after type checking and before emitting. That's something we can do right now without any changes to the TS repo itself, as a separate NPM package that calls and enhances the TS compiler.
There are a few things that need to be done (though this list is certainly incomplete):
const arg = new Foobar(); consumeArg(arg); + arg = undefined;ts.Diagnostics) in caseargis accessed after it is moved before it is assigned a new value.If we constrain ourselves to mutable borrowing (i.e. no notion of immutability) and ownership, there are three major difficulties we need to address:
Regarding some potential solutions posted above: changing the type from
Own<T>toMoved<T>(per Irakli Gozalishvili (@Gozala)) is not something I expect to work well. I think from a semantic perspective, the type should not change after use (per Herrington Darkholme (@HerringtonDarkholme)). We should use the separate borrow checking stage to emit custom errors instead of relying on the type checking stage. However, I agree that usingOwn<T>to signal the compiler that we want borrow checking is probably the best idea to maintain backwards compatibility (i.e. all non-Own<T>values should behave as if there's no borrow checking).emdash commentedon Dec 6, 2019
I'm actually not sure that merely tracking "ordinary" uses of a value (appearing in a statement or expression) is meaningful in typescript. True sub-structural type systems would count any appearance of the value as a "use", and that would complicate a lot of common things. For example, is a method invocation one or two "uses"?
I think it makes more sense to explicitly specify which operations consume a value. Let's keep in mind the actual applications:
step_3, you need aStep2Result, to callstep_2, you need aStep1Result.Here's a relatively simple approach: First, we need types to explicitly annotate sub-structural values. These would be recognized by the compiler:
The global restrictions on identifiers holding a
Resource:const, and not withletorvar(to prevent reassignment, which is assumed by what follows).move().Now we can assume that a sub-structural value can only come from the following sources:
move()callMoreover, once you have a sub-structural value, you must satisfy the requirements for that type:
move()or exactly one call todrop()on a given code path.move()ordrop()on a given code path.And finally,
move()is required in order to:Issues and Edge Cases:
file.close(), notfile.drop(). But it may not be practical to handle this.drop()/move()discipline of the types they capture.SingleUse, then it mustmove()ordrop()the value, and the closure itself then must be called exactly once (like rust's FnOnce).maurizi commentedon Sep 10, 2020
For an example use case of where affine types could prove very useful, they would be excellent for enforcing the behavior of transferable objects sent to Worker threads:
emdash commentedon Sep 17, 2020
nasso commentedon Apr 1, 2023
Being able to model move-semantics would make it possible to safely interact with Web Assembly modules, which typically rely on dynamic allocations a lot.
I suggest being able to do something like this (maybe by special-casing the syntax
T is voidin this context):If using the
voidtype to represent values which shouldn't/cannot be used, we could introduce a specialmovedtype.This would allow us to represent very basic "ownership" semantics like the transferable objects for
Worker.postMessage(), as well as provide extra type-safety when talking with a Web Assembly module (or even just a native system interface). A project likewasm-bindgenwould greatly benefit from this.hazae41 commentedon Sep 9, 2023
nasso (@nasso)
I think I made something great for disposing resources (e.g. freeing WebAssembly pointers)
https://github.com/hazae41/box
Only the owner of the box can dispose the resource
It will throw if you unwrap a moved box
Rudxain commentedon Nov 12, 2024
Why not
never?castarco commentedon Jan 26, 2025
I see that no one mentioned the security-oriented use cases. Statically verifiable security is a quite relevant and clear use case, I'd say it's probably the most relevant one.
A simple example: auth tokens that can be used at most just once (or not used at all).
We can imitate that, but if we want clear guarantees that it won't be bypassed then we need to introduce some sort of runtime error mechanism (it can be via exceptions, or via
Result<R,E>-like type result values, which imposes a small runtime performance penalty).I suspect most people are willing to disregard the performance impact, but not having the ability to statically verify safe execution flows seems a bit more painful to me.
Side note: maybe this could be simulated via comment annotations + a new ESLint plugin (or BiomeJS plugin). If that proved to be useful then we could have a stronger case to convince the relevant devs to make it an official part of TS.
The annotations (for external tools, not for TS) I suspect that would be enough (the names could be different, of course):
CannotCopy: to annotate values, func params, types, interfaces and classes.CannotCopy, because otherwise we could break the annotation-given guarantees once we pass the value to an "unsecured" function.ConsumesSelf: to annotate object methods. If we call one of these annotated methods on an object, then we cannot "use" that object anymore.CannotCopyannotation, because without that other annotation we cannot enforce the rule, and therefore it could create a sense of false security.castarco commentedon Jan 26, 2025
Another feature that could help is to have non-conditional postcondition assertions.
We can already do something like:
If we could introduce non-conditional postcondition assertions, we could avoid having to pass results via param references, and we could avoid having to check for the result value of the function call as well.
I'm not sure how that could be done at the syntax level without breaking any of TS' development rules, though. So I guess this one is mostly for tools such as ESlint or BiomeJS.
Rudxain commentedon Jan 27, 2025
I've been experimenting with non-callable fns in different languages, so I've noticed a problem with using
neveras amovedtypefis actually "callable", we just have to pass a value that TS believes isnever. This behavior is fine for assertions, asfwould never be called at runtime. But ifxwas a "consumed" value, TS allows code that we would consider "unsafe", and JS would have no problem running it.Yes, this is an extremely contrived example. But the fact that it's so simple begs the question of how many cases a
never-to-neverassignment could happen "naturally" in real codeRudxain commentedon Jan 27, 2025
IMO, the type-state pattern is single-handedly the main reason to add move semantics to any lang (RAII is nice, too). I love it so much that I did a major refactor because of it!
Possibly relevant: https://github.com/tc39/proposal-explicit-resource-management
Potential downsides: https://www.gingerbill.org/article/2020/06/21/the-ownership-semantics-flaw
It might also be helpful to take inspiration from Mojo
sjcjoosten commentedon Aug 20, 2025
There's a more common use-case for affine types, functions that mutate an object (adding/removing a property).
With ownership,
delete x.barcould both consume and producex, but with a different type. So,x : T & {bar: any}gets consumed andx : T & {bar?: never}gets produced. If this were howdeleteworked, then we wouldn't need to provide the type ofxfor the example above. For backward compatibility, we might be able to write a function ('affineDelete'?) that does this same operation and is annotated with such types.Some special-casing already makes this work some of the time, I'm not sure how the following example works but to me this current behavior "feels" like typescript has affine types:
I'm glad the second example already works and it is quite robust. Not sure if this can be leveraged for the other examples on this page though.
loynoir commentedon Jun 6, 2026
Stricter ownership type definitely good for audit AI generated code.