-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SUGGESTION: add support for writeonly properties on interfaces #21759
Comments
I can see the use of this as it would allow you to do this without indirection: interface I {
// User of the interface can register a callback, but not call it.
writeonly callback: () => void;
}
class C implements I {
// Only I can call this.
callback: () => void;
} However, in your situation |
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 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. |
Regardless of the behavior you want it to have, requiring classes to implement |
|
Iterator.value | Readable | Writable |
---|---|---|
std.vector.iterator |
O | O |
std.deque.iterator |
O | O |
std.list.iterator |
O | O |
std.set.iterator |
O | X |
std.map.iterator |
O | X |
std.insert_iterator |
X | O |
std.front_insert_iterator |
X | O |
std.back_insert_iterator |
X | O |
The 2nd alternative solution is to defininig the OutputIterator
's type to be general interface allowing both reading
& writing
. However, the solution causes incompleteness error-detecting in compile level.
The OutputIterator (IGeneralIterator)
doesn't guarantee the parameterized Iterator.value
is writable. Even std.set.iterator
, who allows only reading, can be assigned onto the IGeneralIterator
. 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());
@samchon That's a better use-case as it involves using the same type in both readonly and writeonly ways. interface I {
x: number;
}
function copy(input: Readonly<I>, output: Writeonly<I>): void {
output.x = input.x;
} |
@andy-ms Thanks for replying. Anyway, unlike |
Nobody's currently assigned to this issue, so don't expect it to be implemented in the near future. |
@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 |
Found 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 interface ReactRef<T> {
current: T | void
}
interface ReactRefConsume<T> {
writeonly current: T
}
interface Attributes {
ref: ReactRefConsume<HTMLElement>
}
export function createRef<T>(): ReactRef<T> |
I 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? |
+1 Supporting writeonly is probably ideal, but at the minimum, getting a property that only implements a setter should throw an error: #37689 |
Looks 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. type InverseVariance<T> = {
writeonly [P in readonly T]: T[P];
readonly [P in writeonly T]: T[P];
} Just fantasying... @DanielRosenwasser @Igorbek @isiahmeadows |
- remove currently un-typeable ref (probably related to [1] or [2]) - remove `any` typings [1] microsoft/TypeScript#10717 [2] microsoft/TypeScript#21759
- default export - rearrange for shorter functions - remove currently un-typeable ref (probably related to [1] or [2]) - remove `any` typings [1] microsoft/TypeScript#10717 [2] microsoft/TypeScript#21759
I'm currently working on a library where this would be really useful. class Prop<T> {
set(value: T | Promise<T>) { /* */ }
get(): Promise<T> { /* this may involve async processing/validation/sanitization */ }
}
class TwistyPlayerModel {
alg: Prop<Alg> = /* ... */;
puzzle: Prop<string> = /* ... */;
}
class TwistyPlayer extends HTMLElement {
model = new TwistyPlayerModel();
writeonly alg: Alg;
writeonly puzzle: string;
constructor() {
super();
for (const [name, prop] of this.model) {
Object.defineProperty(this, name, {
set: (v) => prop.set(v)
})
}
}
} (See https://github.com/cubing/cubing.js/blob/381fba0859c20387131d2afa5225898c46da829c/src/cubing/twisty/dom/twisty-player-model/props/TwistyPlayerModel.ts#L80-L80 for the real Without But I think it's actually easier to maintain type-safe code the way I wrote above. I can't find of a satisfactory way of tricking the type system into accepting this right now, but I think |
TypeScript doesn't have `writeonly`, so we're keeping the code very explicit for now: microsoft/TypeScript#21759
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. Basically, what we want is |
Would make my day if a missing getter in Typescript was automatically transpiled to return the 'raw' value originally set via the setter. It would be much less surprising to get your original value back out than 'undefined, and would double as a really nice language feature to allow setters to have side-effects without messy boilerplate.
Could be transpiled to something like:
Edit: I realize this isn't going to happen. Just that it would be nice. |
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 |
It's not fully addressed by that in two ways:
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 |
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
|
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) { /*...*/ },
}); |
If it's a data field, how is it "write-only" ? |
@RyanCavanaugh I'm not sure what you're asking. In any case, the syntax noise is the bigger pain at present. |
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. |
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)
|
For me, it is the lack of mapped type support, i.e.:
If we had a built-in |
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) |
I'd like to resurrect an old discussion around the desire to have getters/setters support for interfaces: #11878
Obviously we can use
readonly
to 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 awriteonly
property 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.
The text was updated successfully, but these errors were encountered: