Repository navigation
SUGGESTION: add support for writeonly properties on interfaces #21759
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this feature
on Feb 8, 2018 I didn't use a function because I specifically didn't want multiple listeners. The reason for that is that when a publisher (the child) publishes to its parent, the parent returns a
Promisethat the child can await on to know when the parent has completed processing the request. In our specific use case the child will typically be holding open an HTTP request and waiting for the parent to successfully finish processing the request before ACK'ing to the calling server. If I used a function for this now I have to deal with the potential of multiple subscribers and that will never be the case.To me... This is a classic parent-child case where the parent wants a strong reference on the child and the child needs to maintain a weak reference to its parent. Classically you would potentially pass the parent into the constructor of the child. But in our case the Parent isn't responsible for factoring the child, nor do we want the child to have to know the parents type.
I've been talking somewhat abstractly but if we want to get concrete this is the specific interface in our SDK that I'm referring too:
I'm happy to drill into more specifics and definitely open to suggestions.
writeonlystatementConsideration
Since TypeScript v2.0 update, defining abstract method
getterandsetterare possible. Defininggetterin interface level, it's also possible by thereadonlystatement. However, definingsetterin interface level is not.abstract class AbsClass { public abstract get x(): number; public abstract set y(val: number); } interface ISolution { readonly x: number; writeonly y: number; } function some_function(obj: AbsClass | ISolution): void { // y = x^2 + 4x + 4 obj.y = Math.pow(obj.x, 2) + 4 * obj.x + 4; }
If developer wants to restrict a member to have only
setter(write only), then the developer must define anabstract class, who takes unneccessary dependency. Such dependency can be solved by thewriteonlystatement.Also, within framework of the
Least Privilege Policy,some_functionneeds only reading on memberx, writing on membery. Thesome_functiondoesn't need to writexor ready. Following the policy is possible by implementing thewriteonlystatement.In My Case
I've published a
TypeScript-STLproject, who migrates C++ STL to TypeScript. I agree to this suggestion and I'll be glad if suchwriteonlyfeature comes into the TypeScript. With thewriteonlystatement in interface level, I can make my library to be much stronger.namespace std { // BASIC INTERFACES export interface IForwardIterator<T> { readonly value: T; next(): IForwardIterator<T>; } export interface IOutputIterator<T> { writeonly value: T; next(): IOutputIterator<T>; } // WRITE-ONLY STATEMENT IS THE BEST SOLUTION export function copy<T, InputIterator extends IForwardIterator<T>, // READ-ONLY OutputIterator extends IOutputIterator<T>> // WRITE-ONLY (first: InputIterator, last: InputIterator, output: OutputIterator): OutputIterator { for (; std.not_equal_to(first, last); first = first.next()) { output.value = first.value; output = output.next(); } return output; } }
STL follows Iterator Pattern and the Iterator Pattern is used very a lot in the
<algorithm>functions. Iterators are parameterized and some of them arereadonly, some of them arewriteonly, and some of them are both.For example,
std.copy()is a representative<algorithm>function requires thewriteonlystatement. Without thewriteonlystatement, the first alternative solution is to defining theOutputIteratorto extends anabstract classwho takes the Unneccessary Dependency.Iterator.value Readable Writable std.vector.iteratorO O std.deque.iteratorO O std.list.iteratorO O std.set.iteratorO X std.map.iteratorO X std.insert_iteratorX O std.front_insert_iteratorX O std.back_insert_iteratorX O The 2nd alternative solution is to defininig the
OutputIterator's type to be general interface allowing bothreading&writing. However, the solution causes incompleteness error-detecting in compile level.The
OutputIterator (IGeneralIterator)doesn't guarantee the parameterizedIterator.valueis writable. Evenstd.set.iterator, who allows only reading, can be assigned onto theIGeneralIterator. It causes not compile-error but run-time error,which violates main purpose of the TypeScript.namespace std { export interface IForwardIterator<T> { readonly value: T; next(): IForwardIterator<T>; } export interface IGeneralIterator<T> { value: T; next(): IGeneralIterator<T>; } // WITHOUT WRITE-ONLY, IT CAUSES INCOMPLETENESS ON COMPILE LEVEL export function copy<T, InputIterator extends IForwardIterator<T>, // READ-ONLY OutputIterator extends IGeneralIterator<T>> // DANGEROUS (first: InputIterator, last: InputIterator, output: OutputIterator): OutputIterator { for (; std.not_equal_to(first, last); first = first.next()) { output.value = first.value; output = output.next(); } return output; } } let v: std.vector<number> = new std.vector(4, Math.random()); // GENERAL let s: std.set<number> = new std.set(); // SET.ITERATOR.VALUE IS READ-ONLY s.push(1, 2, 3, 4); //---- // NO ERROR ON COMPILE //---- // BE RUNTIME ERROR // IT VIOLATES MAIN PURPOSE OF TYPESCRIPT std.copy(v.begin(), v.end(), s.begin());
Andy (Andrewkraft) (@Andy-MS) Thanks for replying.
Writeonlyinterface, it sonds interesting. Is it planned to be implemented?Anyway, unlike
readonlyandwriteonlystatement who can specify restriction on member variables level,ReadonlyandWriteonlyinterfaces restrict on object level. Even my example, members onISolutionrequire different types of restriction. The iterators, onlyvaluemembers are required to be restricted.boris-fabernovel commented
on Jun 17, 2018 More actionsAndy (Andrewkraft) (@Andy-MS) : now you really prove us right that typescript is not a superset of javascript anymore, but is slowly but surely becoming another language
Reacted by Eric FerreiraReacted by Kitson Kelly, ૮༼⚆︿⚆༽つ, Eric Ferreira and Derek RiesFound another use case here with getter and setter have different data type.
interface OutgoingRequest { readonly requestBody: string | null writeonly requestBody: string | object }
BTW, this could solve React's
refinvariance issue.interface ReactRef<T> { current: T | void } interface ReactRefConsume<T> { writeonly current: T } interface Attributes { ref: ReactRefConsume<HTMLElement> } export function createRef<T>(): ReactRef<T>
Reacted by Sam A. Horvath-Hunt, Sebastian "Sebbie" Silbermann, Asvel, Anıl Anar, luaneko, Hayden, Sébastien Lorber and Yann KaiserReacted by Sébastien LorberI have also encountered this issue.
It seems as if TypeScript interfaces should be able to fully describe any JavaScript object. This is NOT the case today with properties that can only be set on classes.
class C { constructor(private value: boolean = false) { } set property(value: boolean) { this.value = value; } print() { console.log(this.value) } } interface I { // Suggested Solution: writeonly property: boolean property: boolean; print: () => void; } const e: I = new C(); // Should be allowed e.property = true; // Should NOT be allowed (returns undefined) but it looks like it is. console.log(e.property) e.print();
Are there reasons why implementation of this feature would be difficult or why it wouldn't be a good idea?
Reacted by Tomasz Dysinski and ethernet+1 Supporting writeonly is probably ideal, but at the minimum, getting a property that only implements a setter should throw an error: #37689
Reacted by ethernet, wandyezj and Mathieu LemoineReacted by James BromwellLooks like it could be a reasonable semi-measure that will allow somehow model a covarince/contravariance (that I suppose will never be implemented in TS)
type AnyFunction = (...args: writeonly never[]) => unknown
It's kinda dangerous, yet looks better than
type AnyFunction = (...args: any[]) => unknown
However, for the full support it should be properly integrated with mapped types.
something like this:type InverseVariance<T> = { writeonly [P in readonly T]: T[P]; readonly [P in writeonly T]: T[P]; }
Just fantasying...
Daniel Rosenwasser (@DanielRosenwasser) Igor Oleinikov (@Igorbek) @isiahmeadows
?Reacted by Claudia Meadows, Benjamin Atkin, Artur Klesun and HaydenReacted by wandyezj, Benjamin Atkin and Hayden6 remaining items
That's not going to happen, as it a) is the opposite of what native JS classes do in this case, and b) violates the Design Goals (and non-goals).
I'd like to see the property treated as writeonly; failing that, get yourself a linter and set up the
accessor-pairsrule. At least you'll be forced to write out some kind of getter any time you make a setter, and your code will behave consistently.I think this could be addressed by #43662 so I just want to make sure anybody subscribed here also wanders over there at some point.
It's not fully addressed by that in two ways:
- You need mapped types for a generic utility type, but you can't define getter and setter on a mapped type. (See Computed key in get, set declarations incorrectly reported as non-key type #51890 (comment))
- It still doesn't prevent reading and checking against null values, even if the return type is
never.
So I think it would still be good to have writeonly properties even when we can already have different types between getter and setter.
This would be a big step towards supporting contravariant interfaces. A common use case is React refs - the
currentfield is supposed to be write-only when passed to a component, but Typescript doesn't support that and all refs are incorrectly treated as covariant.Reacted by Claudia Meadows and Alex ReardonRyanCavanaugh commented
on Mar 4, 2024 MemberMore actionsWhat shortcomings are there with using this?
interface Foo { get foo(): never; set foo(x: string); }
What shortcomings are there with using this?
interface Foo { get foo(): never; set foo(x: string); }
It doesn't accurately describe the data structure, and the following are syntax noise compared to a simple
writeonlymodifier:- two fields vs. one
getset- method parentheses
- setter parameter name (
xin example)
Reacted by bongdungyeuem27RyanCavanaugh commented
on Mar 4, 2024 MemberMore actionsIt doesn't accurately describe the data structure
How so?
How so?
Data descriptors and accessor descriptors aren't identical, even though they might appear indistinguishable to consumers.
Such discrimination might potentially provide value if TypeScript were to support this in the future — for example — as an implementation constraint that allows a plain writable data descriptor, but not a setter accessor descriptor (maybe to prevent possible side-effects in an implementation). Imaginary syntax example:
type WriteonlyFoo = { writeonly foo: string }; // Allowed const o: WriteonlyFoo = Object.defineProperty({}, "foo", { writable: true }); // Not allowed const o: WriteonlyFoo = Object.defineProperty({}, "foo", { set(value: string) { /*...*/ }, });
RyanCavanaugh commented
on Mar 4, 2024 MemberMore actionsIf it's a data field, how is it "write-only" ?
If it's a data field, how is it "write-only" ?
Ryan Cavanaugh (@RyanCavanaugh) I'm not sure what you're asking. In any case, the syntax noise is the bigger pain at present.
If it's a data field, how is it "write-only" ?
Because that's the intended contract. Or because it improves reasoning about variance. Or because it provides parity with accessors. Same way a property can be protected, even though there's really not such a thing. That said, I don't know to what degree write-only is actually enforced. You can certainly write to a property that only defines a setter without any complaints from the type checker.
What shortcomings are there with using this?
interface Foo { get foo(): never; set foo(x: string); }
It doesn't make Foo contravariant in regards to its foo property.
type WriteOnlyRef<in T> = { set current(current: T) get current(): never } function setRefToCanvas(ref: WriteOnlyRef<HTMLCanvasElement>) { ref.current = document.createElement("canvas") } type Ref<T> = { current: T } const ref: Ref<HTMLElement | null> = { current: null } setRefToCanvas(ref)
Argument of type 'Ref<HTMLElement | null>' is not assignable to parameter of type 'WriteOnlyRef<HTMLCanvasElement>'. Types of property 'current' are incompatible. Type 'HTMLElement | null' is not assignable to type 'never'. Type 'null' is not assignable to type 'never'.Reacted by Tamás Hegedűs and Ryan CavanaughWhat shortcomings are there with using this?
interface Foo { get foo(): never; set foo(x: string); }For me, it is the lack of mapped type support, i.e.:
type Writeonly<T> = { set [K in keyof T](value: T[K]) }; // error, cannot declare get or set types in mapped typesIf we had a built-in
Writeonlytype, that would suffice, I could use it to cobble together any type I wish.Reacted by Claudia Meadows, snarbies and Craig Macomber (Microsoft)CraigMacomber commented
on Jul 17, 2024 More actionsWhat shortcomings are there with using this?
interface Foo { get foo(): never; set foo(x: string); }For me, it is the lack of mapped type support, i.e.:
type Writeonly<T> = { set [K in keyof T](value: T[K]) }; // error, cannot declare get or set types in mapped typesIf we had a built-in
Writeonlytype, that would suffice, I could use it to cobble together any type I wish.If thats the only issue, then maybe this thread can be considered a duplicate of #43826 which tracks the specific limitation of mapped types not allowing control of setters (and includes a suggestion to solve it with "writeonly" as one of the the 5 ways I propose to solve it)
Note
I raised this here as Mateusz Burzyński (@Andarist) mentioned that "writeonly properties" would be the solution to this issue. Please let me know if I should raise this elsewhere. Conversation on bluesky
It looks like "writeonly" properties would be helpful for the
refprop inreact:Problem
import React, {useRef} from 'react'; function App() { const myRef = useRef<HTMLElement | null>(null); return <button ref={myRef}>hi</button> // ^ Why can I not use a RefObject<HTMLElement> here? // It is safe for me to treat a HTMLButtonElement as a HTMLElement as // HTMLButtonElement is a subclass of HTMLElement. // What am I missing? }
Event though the assignment
myRef.current = buttonElementwould be allowed (HTMLButtonElementextendsHTMLElement),myRefdoes not satisfy the constraint of{current: HTMLButtonElement | null}Problem elaborated
const first: {current: HTMLButtonElement | null} = {current: null} const second: {current: HTMLElement | null} = {current: null} function assign(ref: {current: HTMLButtonElement | null}) { const button: HTMLButtonElement = document.createElement('button'); ref.current = button; } // ✅ first satisfies constraint of `{current: HTMLButtonElement | null}` assign(first); // ❌ second does not satisfy constraint needed for the `assign` argument assign(second); function getButton(): HTMLButtonElement { return document.createElement('button'); } // ✅ both `first.current` and `second.current` can have a HTMLButtonElement // assigned to them, as HTMLButtonElement extends HTMLElement first.current = getButton(); second.current = getButton();
Reacted by Claudia Meadows and Luke Deen Taylorguillaume-mueller commented
on Aug 16, 2026 More actionsWhat the hell is going on here. We need to KISS.
const obj = { set prop(value: any) {} }; obj.prop; // TypeScript doesn't complain
It just is a bug.
If TS is a superset of JS, then respect JS.
Reacted by Claudia MeadowsReacted by snarbles2Reacted by valepu
I'd like to resurrect an old discussion around the desire to have getters/setters support for interfaces: #11878
Obviously we can use
readonlyto express a property on an interface that has just a getter but there's no way of expressing that a property should have just a setter. In issue #11878 there was an idea proposed to add the concept of awriteonlyproperty designed to cover this need but the consensus was that there wasn't enough real-world scenarios to justify such a feature. So let me try and add one.We have a situation where we have a child object that we want to publish data to a parent but while we want the parent to know about its child we don't want the child to know about its parent. We've ruled out the use of events because we only want a single subscriber and we need to return a Promise to the child to let them know when we're done. Instead we've opted to establish what we would have called a "weak reference" between the child and parent back in the days of COM. The interface in TypeScript looks something like this:
As you can see data flows bidirectionally between the parent and child and while we've received a couple of questions about why the interface is the way it is, it's generally easy enough to grok from a TypeScript perspective.
The issue we just ran into, however, is that a developer on our team just created a class in ES6 that implements this interface and the result ended up being.... yuck :(
If we literally implement this interface in a declarative way in ES6 it looks something like:
Not only is it crappy that you have to define a getter and a setter, the fact of the matter is we're never going to ask for the callback back so the getter is pointless here. So what did our dev do? He did this:
That technically works and what's nice is you have some sense of the signature of the handler but it makes my skin crawl to look at it. If I was to mirror that in my TypeScript interface you'd have zero clue that
onDataReceived()was something I expect you to override. What I really want the developer to have to write implementation wise is this:That's the proper contract for a weak reference but I have no way of expressing it in TypeScript. While it's very rare that you need to do this it doesn't make it any less valid a scenario. The addition of "writeonly" properties would give me a way to express this.