Typescript: Type Operators Support

Created on 24 May 2019  ·  18Comments  ·  Source: microsoft/TypeScript

Feature Proposal

Modern JavaScript and Typescript syntax allows easy support for type operators, similar to that in C++, when a property of certain type can be implied whenever the type is needed.

By analogy with C++, I suggest to add word operator, to be treated the same as get, but to be also used for the type resolution.

Example:

class A {
}

class B {
    private prop1: A;
    private value: number;

    operator name1(): A {
        return this.prop1;
    }

    operator name2(): number {
        return this.value;
    }
}

Using operators:

function test1(obj: A) {
}

function test2(val: number) {
}

const obj = new B;

test1(obj); // translated into test1(obj.name1)

test2(obj); // translated into test2(obj.name2)

// we can use those as regular get operators also:
const a: A = obj.name1;
const b: number = obj.name2;

TypeScript would treat operator in the same way as get, i.e. by simply replacing operator with get, while allowing automatic resolution of type by redirecting to the right get method.

Also, during the type parsing, TypeScript will detect and report any case where the type resolution is ambiguous, which should be fairly easy. For example, if a class implements an operator with the same return type more than once, this is a definitive type ambiguity.

Another example of type ambiguity to be reported as error would be when multiple types are supported at the same time:

class A {
}
class B {
    private prop1: A;
    operator name1() : number {
    }
}

// TEST:
function test(val: B | number) {
}

const obj = new B();

// the following line should throw a type ambiguity error,
// because it cannot choose which to pass in - the object or its property name1:
test(obj);

  • [x] This could be implemented without emitting different JS based on the types of the expressions

There only would a replacement of operator with get, nothing else.

  • [x] This isn't a runtime feature (e.g. library functionality, **non-ECMAScript syntax with JavaScript

We only substitute an object with object+property, and that's it.

Out of Scope Suggestion

Most helpful comment

As others have pointed out, this is absolutely type-directed emit and thus outside the scope of things we're willing to add to the language.

All 18 comments

This violates several points on the list of check list you deleted from the suggestion template. These guidelines are not met:

  • [ ] This could be implemented without emitting different JS based on the types of the expressions
  • [ ] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)

@dragomirtitian I am updating it right now, please bear with me... will include those shortly.

@dragomirtitian Does this look ok now?

@vitaly-t I am not the one you have to convince. But the major hurdle I see for it is first check point. The fact that it requires type information for correct emit. This will prevent tools like Babel that process one file at a time (and have no knowledge of other files) to correctly emit this code.

Consider your code:

const obj = new B;

test1(obj); // translated into test1(obj.name1)
test2(obj); // translated into test2(obj.name2) 

By looking at this and only this how do I know what js will be emitted? Unless I have access to the definition of B I don't know what to emit.

FYI: Const enums which are part of TS require such type information from other files and are not supported in babel for this exact reason.

@dragomirtitian Ok, so there may be some problems for the Babel platform, I understand.

But is it a reason enough not to consider such a useful feature in TypeScript? After all, TypeScript is a generic platform, not one created for Babel.

So maybe it can be disabled for Babel via a key in tsconfig.json, to simply turn every operator into get, without any type resolution? In this case it simply won't allow any operator usage, and whenever it is needed, will be reported as type error, because it won't be able to resolve the type, and that's it! This actually can make the feature fully backward compatible.

@dragomirtitian

So we can use either something like allowOperators or disallowOperators inside tsconfig.json, depending on which one makes better sense - to have the feature by default, while allowing to disable it, or the other way round.

And as I stated above, disallowing operators would simply result in the standard type mismatch error, as every operator would become a simple get, without any type resolution.

@vitaly-t That would open up a whole can of worms I don't think anyone wants to deal with. React and vue both use babel to do transpilation and ts just for type checking. Going down the path of feature A is available here while feature B is not available here would end up fragmenting ts a lot. What if you have packages that depend on such a feature, you cold not use it properly from one environment or the other.

Anyway, the that was the biggest hurdle to implementation, the fact that it is non-ECMAScript syntax with JavaScript output will probably kill it being considered at all. TS just wants to add types to JavaScript it avoids adding new syntax in expressions. (In the beginning there we some non standard features such as enums but that was just TS's rebellious teen years 😜)

@dragomirtitian

It is a worthwhile feature that improves TypeScript. If we start with the presumption that TypeScript cannot advance, unless Babel keeps working as it is, you might as well park the project and say amen.

it is non-ECMAScript syntax with JavaScript output

What do you mean? The output if fully compatible, with the standard get operator.

Long story short: type-directed emit is an explicit non-goal of the TS project (for reasons other than Babel interop), and anything that requires type information to emit correct code will likely not see the light of day.

https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals

  1. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

Babel is a red herring here. This rule exists for smooth interop with JS, where static typing doesn't exist.

As for "non-ECMAScript syntax with JavaScript output", this means exactly what it sounds like - "syntax which is not legal in plain JavaScript that results in actual code being emitted". TypeScript types are fully erasable, so they have no representation in the emitted code. See #31300 for a similar proposal that was shot down for the same reasons. In the end it boils down to this: If you can write code in TS, you should be able to write the same code in plain JS--sans type annotations--and it will function identically. This...

test1(obj); // translated into test1(obj.name1)
test2(obj); // translated into test2(obj.name2) 

...doesn't exactly fit that criteria.

@fatcerberus Why do you guys keep talking about run-time type information, when the feature suggested here isn't for run-time at all. The use of operator/get suggested here is for the transpiler to simply replace operator with get and the variable with the property name when needed according to the type. This is NOT dynamic, this is NOT run-time.

Wait-wait-wait. @vitaly-t, what happens here?

class B {
    private value: number;
    private value2: number;

    operator name2(): number {
        return this.value;
    }

    operator name3(): number {
        return this.value2;
    }
}

/* ------------------------------- */

function test(val: number) { // irrelevant }

const obj = new B();

test(obj); // is it obj.name2 OR obj.name3 ?

This is NOT dynamic, this is NOT run-time.

And that’s precisely the point. The example function calls above mean different things when written in TS vs. plain JS when targeting your hypothetical operator-overloaded library, which breaks JS/TS interop. TS’ goal is to “JS with types” so changing the meaning of code based on the existence of type info goes against that.

@tonisostrat Such case was precisely described in the initial proposal, with an example, the same as you did here. This one is to throw an ambiguity error. Only one operator of the same type would be usable when it is needed. It is quite normal, just like in C++ where you also get a type ambiguity error.

@tonisostrat Such case was precisely described in the initial proposal, with an example, the same as you did here. This one is to throw an ambiguity error.

I apologize, you did cover it, I can see it now.

Anyway, in that case this proposal is pretty much pointless anyway as the instances in real life where you only have classes with unique and singular type properties is very limited, if not practically non-existant.

Also, this would make code horribly unreadable as I would have to actually look at the definition of the method to see which property in my class is actually being referenced and it's still confusing if I don't have a complete mental map of that class to instantly know whether the number is value1 or value2.

It seems like you have some niche case where you want to make it easier for you to write code and that's it.

in real life where you only have classes with unique and singular type properties is very limited, if not practically non-existant.

It is needed all the time. For any type that encapsulates a number of properties, it would want to expose in various context implicitly, according to the type needed.

@vitaly-t, can you give us an actual sample of production code where this would be useful?

As others have pointed out, this is absolutely type-directed emit and thus outside the scope of things we're willing to add to the language.

That is very unfortunate, to be limited by a policy that prevents from getting useful features added.

I love using this feature in C++, it is quite powerful, allowing multi-typing without explicit references, and I still think it would make a good addition in TypeScript.

Here's an example from my real code (a trading platform):

class CandleList {

    private data: Candle[]; // sometimes it is virtualized

    // actual code being used:
    get getCandles() : Candle[] {
        // some pre-processing here...
        return this.data;
    }

    // wishful thinking:
    operator getCandles() : Candle[] {
        // some pre-processing here...
        return this.data;
    }
}

const list = new CandleList();

function processCandles(candles: Candles[]) {
}

// I wish I could just write this:
processCandles(list);
// because that type is encapsulated and exposed

// instead, I have to do this everywhere:
processCandles(list.getCandles);
Was this page helpful?
0 / 5 - 0 ratings

Related issues

weswigham picture weswigham  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

bgrieder picture bgrieder  ·  3Comments

uber5001 picture uber5001  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments