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 a writeonly 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:
interface Adapter {
onDataReceived: (data: any) => Promise<void>;
publishData(data: any): Promise<void>;
}
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:
export class WebAdapter {
get onDataReceived() {
return this.callback;
}
set onDataReceived(cb) {
this.callback = cb;
}
postData(data) {
}
}
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:
export class WebAdapter {
onDataReceived(data) {
// will be replaced by parent
}
postData(data) {
}
}
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:
export class WebAdapter {
set onDataReceived(cb) {
this.callback = cb;
}
postData(data) {
}
}
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.
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 this.callback is presumably a private member, written to in set onDataReceived. In which case, why not just have a onDataReceived(cb) method? Having a method would allow registering multiple callbacks instead of writing over the current one.
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 Promise that 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:
https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botbuilder/src/activityAdapter.ts
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 set onDataReceived as a setter doesn't seem to accomplish more than requiring setOnDataReceived as a method would. A property that consists only of a setter is basically just a method with one parameter.
writeonly statementSince TypeScript v2.0 update, defining abstract method getter and setter are possible. Defining getter in interface level, it's also possible by the readonly statement. However, defining setter in 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 an abstract class, who takes unneccessary dependency. Such dependency can be solved by the writeonly statement.
Also, within framework of the Least Privilege Policy, some_function needs only reading on member x, writing on member y. The some_function doesn't need to write x or read y. Following the policy is possible by implementing the writeonly statement.
I've published a TypeScript-STL project, who migrates C++ STL to TypeScript. I agree to this suggestion and I'll be glad if such writeonly feature comes into the TypeScript. With the writeonly statement 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 are readonly, some of them are writeonly, and some of them are both.
For example, std.copy() is a representative <algorithm> function requires the writeonly statement. Without the writeonly statement, the first alternative solution is to defining the OutputIterator to extends an abstract class who takes the Unneccessary Dependency.
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. Writeonlyinterface, it sonds interesting. Is it planned to be implemented?
Anyway, unlike readonly and writeonly statement who can specify restriction on member variables level, Readonly and Writeonly interfaces restrict on object level. Even my example, members on ISolution require different types of restriction. The iterators, only value members are required to be restricted.
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 ref invariance issue.
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: https://github.com/microsoft/TypeScript/issues/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.
something like this:
type InverseVariance<T> = {
writeonly [P in readonly T]: T[P];
readonly [P in writeonly T]: T[P];
}
Just fantasying...
@DanielRosenwasser @Igorbek @isiahmeadows
?
Most helpful comment
BTW, this could solve React's
refinvariance issue.