Open icholy opened 9 years ago
What specific use cases do you have in mind here?
We had a similar discussion a long time ago -- that the rules for the math operators should be written in terms of the valueOf
members of the apparent type of the operands rather than their types. It wouldn't be a breaking change and would enable this and some other good scenarios. I believe the only issue there was the behavior of Date
values.
We had a similar discussion a long time ago
Is there a public thread somewhere I can check out?
I believe the only issue there was the behavior of
Date
values.
What issue are you referring to?
It was an internal discussion before we went public.
Date
is insane and I think why we backed off from doing this. Its valueOf
method produces a number
, but its [[DefaultValue]]
internal method defaults to a string
hint type (http://www.ecma-international.org/ecma-262/5.1/#sec-8.12.8). This results in very surprising behavior:
> var x = new Date();
undefined
> x.valueOf()
1426616370842
> x + x
"Tue Mar 17 2015 11:19:30 GMT-0700 (Pacific Daylight Time)Tue Mar 17 2015 11:19:30 GMT-0700 (Pacific Daylight Time)"
> x - x
0
> x + 0
"Tue Mar 17 2015 11:19:30 GMT-0700 (Pacific Daylight Time)0"
> x - 0
1426616370842
> x * x / x
1426616370842
> x * x + x
"2.0352342695543988e+24Tue Mar 17 2015 11:19:30 GMT-0700 (Pacific Daylight Time)"
:cry:
Oh man ... I didn't know that.
What if Date
was treated as a special case? Either don't allow it, or have the compiler yell at you.
Would like to see this as e.g. a change to the spec + implementation that specifies exactly how this might work
I need this one 👍
Hi,
I am currently contributing to the typings
project and I encountered this issue when writing definitions for the big-integer
npm package. This package allows you to use arithmetic on their wrapper object trough implicit calls to toJSNumber
thanks to valueOf
.
Here is their documentation.
This is just an other use-case for this issue and I'm hoping that there will be a solution.
@RyanCavanaugh I noticed that in the es6 typings, the Date
interface has [Symbol.toPrimitive](hint: "default")
declared. That should let us do something like this.
type BoxedValue<T> = {
valueOf(): T;
}
type DefaultValue<T> = {
[Symbol.toPrimitive]?(hint: "default"): T;
}
type Operand<T> = T | BoxedValue<T>;
type StrictOperand<T> = T | (BoxedValue<T> & DefaultValue<T>);
These operators require their operands to be of type Any, the Operandnull
or undefined
value, it is treated as having the type of the other operand. The result is always of the Number primitive type.
Any | Boolean | Operand\<Number> | String | Other | |
---|---|---|---|---|---|
Any | Number | Number | |||
Boolean | |||||
Operand\<Number> | Number | Number | |||
String | |||||
Other |
The binary + operator requires both operands to be of the StrictOperandnull
or undefined
value, it is treated as having the type of the other operand. If both operands are of the Number primitive type, the result is of the Number primitive type. If one or both operands are of the String primitive type, the result is of the String primitive type. Otherwise, the result is of type Any.
Any | Boolean | StrictOperand\<String> | Operand\<String> | Other | |
---|---|---|---|---|---|
Any | Any | Any | Any | String | Any |
Boolean | Any | String | |||
StrictOperand\<Number> | Any | Number | String | ||
Operand\<String> | String | String | String | String | String |
Other | Any | String |
@RyanCavanaugh I did (an extremely hacky) implementation and it does seem to work https://github.com/Microsoft/TypeScript/compare/master...icholy:master
In my own code I'll probably start using something like this:
class Time extends Date {
[Symbol.toPrimitive](hint: "default"): number;
[Symbol.toPrimitive](hint: string): string | number {
switch (hint) {
case "number":
return this.valueOf();
case "string":
return this.toString();
default:
return this.valueOf();
}
}
}
Another use case we're looking at is MoneySafe.
Its type definition is like below:
declare module 'moneysafe' {
export const m$: (options?: {
centsPerDollar: number,
decimals: number,
symbol: string,
round: (x: number) => number,
}) => Money;
export const $: MoneyFunc;
export const in$: (cents: number) => number;
interface MoneyFunc {
(dollars: number): Money;
of: (cents: number) => Money;
cents: (cents: number) => Money;
}
interface Money {
valueOf(): number;
cents: number;
$: number;
round(): Money
add(a$: number): Money;
subtract(a$: number): Money;
toString(): string;
}
}
And here's how it's used:
import { $ } from 'moneysafe';
let cents = $(.2) + $(.3); // TS error: Operator '+' cannot be applied to types 'Money' and 'Money'.
console.log(cents); // Works at runtime. Outputs: 50
@omidkrad : My case is similar to yours. I was forced to used "any". It's not good at all.
If you could add a extra diagnostics line in the compiler output before the error with the meaning "to get this done use .valueOf()" and everybody is happy?
@sdrsdr no, consider this code:
var startTime: Date = null; // read it from db and it was null
var duration = (new Date() - startTime) / 1000;
if (duration > 10) {
console.log('do something');
}
If startTime
comes as null
then above code won't break, but if you use startTime.valueOf()
or startTime.getTime()
in the calculation then it will throw error.
Uncaught TypeError: Cannot read property 'valueOf' of null
So we have to add null checks everywhere to avoid the error. Besides, adding unnecessary valueOf()
everywhere will make formulas (such as moneysafe
calculations) ugly and hard to read. I think it would be best if TypeScript just allows this, because it is valid JavaScript.
Not handling nulls from outside world is an error in itself. This particular code will be better handled with Date.now() a single static call than constructor plus a hiden .valueOf() that calls .getTime(). I grew a huge dislike to automagical typecasting since my C++ years.
Has anything happened on this?
I have a class of Unit
that when used as a string e.g. width:${width}
returns width:10px;
and using valueOf
should be mathable e.g. width * 2 // 20
. This works fine as plain JS but not as TS due to Unit
not being any
, number
or enum
.
Is there a way to type this so it works? I've tried @icholy's approach with the [Symbol.toPrimative]
typing to no avail.
@Arcath the [Symbol.toPrimative]
would fix the issue with Date
if this feature was ever added.
typescript-extra is a library ( brand new ) that hopes to provide users with extra types like
You name it! As well as presenting an easy way of creating which ever new type you might need. If this is not enough to convince you, check out the readme.
As with BigInt the users of typescript-extra are forced to used valueOf
to explicitly get the number primitive which is wrapped in an object e.g.
1 + new Int(1).valueOf()
Even though the code effortlessly compiles without the use of valueOf
1 + new Int(1)
@RyanCavanaugh What exactly constitutes a proposal? How specific do I have to be? Anyway...
I propose that, as stated by Ryan, the rules for the math operators should be written in terms of the valueOf
members of the apparent type of the operands rather than their type.
This means that arithmetic expressions would be allowed for the number
, bigint
, string
, any
types and any other type that has a valueOf
member whose return value is of type number
, bigint
or string
. The type's internal [[DefaultValue]]
is disregarded on this matter (for types other than number
, bigint
, string
and any
, of-course).
If a both operands of a mathematical expression satisfy this condition, but their respective valueOf
return types are of incompatible numeric types (number
and bigint
) the operation is not allowed.
I would advice for the conversion to be executed at the transpiling level, to avoid any issues with [[DefaultValue]]
. For example, x + 4
would transpile to x.valueOf() + 4
.
@RyanCavanaugh this strikes me as a bug. TypeScript shouldn't choke on valid JavaScript. It seems weird that this issue is still open after 4 years, especially considering that the community has provided multiple use cases, proof-of-concept code, and a specification.
FWIW the use case that has led me here is the Counter class in Automerge, which looks something like this:
class Counter {
constructor(value) {
this.value = value || 0
Object.freeze(this)
}
/**
* A peculiar JavaScript language feature from its early days: if the object
* `x` has a `valueOf()` method that returns a number, you can use numerical
* operators on the object `x` directly, such as `x + 1` or `x < 4`.
* This method is also called when coercing a value to a string by
* concatenating it with another string, as in `x + ''`.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf
*/
valueOf() {
return this.value
}
toString() {
return this.valueOf().toString()
}
increment(delta) {
delta = typeof delta === 'number' ? delta : 1
this.value += delta
return this.value
}
decrement(delta) {
return this.increment(typeof delta === 'number' ? -delta : -1)
}
}
TypeScript chokes on any use of a Counter
object as a number, even though the code executes as expected:
const c = new Counter(3)
c.increment()
assert.strictEqual(c < 4, true) // Operator '<' cannot be applied to types 'Counter' and 'number'. ts(2365)
assert.strictEqual(c + 10, 13) // Operator '+' cannot be applied to types 'Counter' and '10'.ts(2365)
The only workaround I've found is to force-cast the counter to number
:
assert.strictEqual(c as unknown as number < 4, true)
assert.strictEqual(c as unknown as number + 10, 13)
I'm using this ugly:
type A = number & { prop }
let a: A
a + 1
Yet still waiting for any update from the issue.
Quite a surprise we've got this broken. It'd be useful to(for omitting details) have the ability to get default/main value just by using the object with user-defined valueOf/toString methods, and not accessing its fields.
Another user having problems with this. I'm fiddling with the compiler API, and fixed the problem (somehow) when valueOf
has no generics (for example: valueOf():number
), but I'm struggling a lot when valueOf
is something like this: valueOf<T>(this:T): T extends (...args:any) => any ? ReturnType<T> : T;
.
What I've done is, when is going to check the left and right types of an operation, look for the valueOf()
method and extract its return value, like so:
const symbol = getPropertyOfType(left, "valueOf");
const type = symbol && getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
const signature = type && getSignaturesOfType(type, 0)[0];
left = (signature && signature.getReturnType()) || left;
This will make left
operand to be the valueOf()
return value, so checks below this code will work as intended.
Lets see if someone can throw a bit of light on how to get the final type of this type: T extends (...args:any) => any ? ReturnType<T> : T
.
Bumped into this again today. Surprised there has been no traction on this.
Not sure if this is the same problem but when I got that error signature I just changed it from
BigInt + BiggerInt
to BigInt.plus(BiggerInt)
I'm using this ugly:
type A = number & { prop } let a: A a + 1
Yet still waiting for any update from the issue.
This failed in the case where I wanted to to do type Id = string & IdInternal
where IdInternal extends Uint8Array
. When I later tried id[0] = 4
, TS complained that id[0]
is type never
.
Why is this still open? I think that after 7 years at least a resolution "yes, we'll implement" or "no, won't do" should be defined, no?
I believe that it should be implemented, especially because it is valid JS and, by the number of issues referencing this one and the amount of comments presenting use cases, it's clearly seen as a bug in Typescript
On my case, I'm using mongoose's Decimal128 type, which converts to number normally, but since typescript won't let me I have to add a bunch of +
as in +decimalNumber > normalNumber
every time I wants to compare the two
As far as I understand from this thread, the only problem with this new feature is the Date object, which is fair. So, if it is possible to add an exception to the rule, not allowing a date + date
or something, I think that it should be done, at least for now. Date is a nightmare anyway
But, if adding an exception is not possible, then I think that at least this issue should be closed with a "won't implement because
This is a confusing issue for even simple BigInt usage:
function f0(int: BigInt): BigInt {
return BigInt(int) // Error: Argument of type 'BigInt' is not assignable to parameter of type 'string | number | bigint | boolean'.(2345)
}
console.log(f0(BigInt(1))) // OK: 1
A workaround is:
function f1(int: BigInt): BigInt {
return BigInt(int.valueOf()) // OK
}
console.log(f1(BigInt(2))) // OK: 2
This is a confusing issue for even simple BigInt usage:
function f0(int: BigInt): BigInt { return BigInt(int) // Error: Argument of type 'BigInt' is not assignable to parameter of type 'string | number | bigint | boolean'.(2345) } console.log(f0(BigInt(1))) // OK: 1
A workaround is:
function f1(int: BigInt): BigInt { return BigInt(int.valueOf()) // OK } console.log(f1(BigInt(2))) // OK: 2
The problem here is that BigInt(X)
does not return a BigInt
but a bigint
. BigInt
is not something you can instantiate. For example, you cannot do new BigInt(1)
but you can do new Number(1)
(which new Number(1)
is different from Number(1)
). You will also notice how BigInt(1) instanceof BigInt
is not true.
The real workaround here is:
function f0(int: bigint) { // The return type is already "bigint"
return BigInt(int)
}
console.log(f0(BigInt(1)))
I'm using primitive type coercion in my Java Runtime Environment emulation, to provide the same experiences for TS users like there are for Java users. In the Boxing and Unboxing chapter I explain valid use cases, which none-the-less are defeated by the TS compiler.
For a typical use case check the implementation of the java.lang.Long
class
@Llorx you are talking about wrapper objects, right?
As @mike-lischke says, they can be used to box/type-cast values as used in java and C# but they are not implemented in the same way in JS.
None of the wrapper objects are meant to be used with the new
key word but you can and JS will allow it since you can subclass a wrapper value and in that case you need to call the constructor, which new
is syntactic sugar for. Each wrapper object may have different implementations in regard to sub classing. E.g. for BigInt
:
is not intended to be used with the new operator or to be subclassed. It may be used as the value of an extends clause of a class definition but a super call to the BigInt constructor will cause an exception. ~https://262.ecma-international.org/#sec-bigint-constructor
Historically wrapper objects were suppose to be instantiated with
new
but this have since been removed from the specification. IIRC 2011ish but all browsers supported wrapping without the use ofnew
. ~@dotnetcarpenter
This issue is about valueOf
which is called implicitly when you use arithmetic operators. E.g. +
, -
, /
or *
. This is not to be confused with what happens when you try to add (with +
) an object to a string
, where the toString
method is called. E.g:
const def = {
toString () { return "def" },
valueOf () { return 123 }
}
console.log (
// When String is called as a function rather than
// as a constructor, it performs a type conversion.
("abc" + String (def)), // <- "abcdef"
// In TypeScript you will get an error below saying
// Operator '+' cannot be applied to types 'number' and '{ toString(): string; valueOf(): number; }'.
(1 + def), // <- 124
)
The use of valueOf
, to return a primitive value that is not a String, has been in JS since, at least, version 3 while TypeScript has never had this.
const dateA = new Date ("1981-04-10T10:00:00")
const dateB = new Date ("1981-04-10T12:00:00")
console.log ((dateB - dateA) / 1000 / 60 / 60 + " hours") // <- "2 hours"
/* TypeScript Errors:
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
*/
PS. Makes you wonder how TypeScript can be a super language of JS, when it has never fully supported JS.
PPS. Weirdly enough TypeScript does allow +
for dates as long as the hint is string. Meaning that TypeScript is happy as long as the conversion is to string and not number.
console.log (
"Today's date is " + new Date())
// "Today's date is Thu Dec 29 2022 19:30:56 GMT+0100 (Central European Standard Time)"
No updates on this whatsoever?! It's a highly requested feature and is almost 8 years old. Please at least give us a hint or two as to whether or not it's being planned.
Thanks.
Our iteration plans are public and you can check them to see if things are in there; no hints are needed. I don't think it's particularly likely to happen soon given that doing these operators on wrapped values is somewhat rare, and Date
throws a big wrench in the works as mentioned above.
In general if an issue is old, it's probably old for reasons that can be found in the thread. Language design is not a first-in first-out queue and things which don't seem tractable "for now" are generally left open since people complain more if we close them.
Also a +1 for this. The compiler should recognize the presence of valueOf
and allow its usage as per the JS standard.
I agree that JavaScript works in weird ways (https://github.com/microsoft/TypeScript/issues/2361#issuecomment-82412122), but Date
is definitely not the only problem JavaScript has, and TypeScript is supposed to be a superset of JavaScript (https://github.com/microsoft/TypeScript/issues/2361#issuecomment-1367497616). So to me it comes as a surprise to see that Date.now() < x
compiles in JavaScript, but does not compile in TypeScript.
I have to add though that now with the former explanation, implicit type coercion in JavaScript has its traps, and when you have to understand the specification to figure out how code like Date.now() < x
works (especially compared to x + x
), I think that's just code that's trying to be too smart and therefore it's impractical (see also: principle of least astonishment), so I am now more inclined to just go with the simpler Date.now() < x.getTime()
, at least until Temporal
becomes part of the EcmaScript standard... But for the same reason, the error we see in TypeScript could still be a warning, or something for ESLint to catch. (E.g., we could use an appropriately configured no-implicit-coercion
rule, but the rule by default disallows convenience expressions like !!''
too, so I see why it's not part of the eslint-recommended ruleset. Plus, for understandable reasons it does nothing about Date.now() < x.valueOf()
, which I also consider puzzling. Oh wow, to me it looks like I'm starting to open Pandora's box with this, so I'll stop here.)
This is very frustrating.
I have a custom "dynamic properties" implementation where objects can hold various values or collections of them (for purposes like change events, de/serializing, view delegates, etc). These dynamic property objects can easily return whatever type they actually hold, including primitives, using valueOf()/toString()/[Symbol.toPrimitive]()
overrides. But not in TS.
I don't understand the argument that it's not a "commonly" used feature... those are rather major features of the JS/ECMA. This is why they can be overridden, right? We don't have direct access to [[Get]]
and [[Set]]
(as far as I know), so those are the next best thing.
declare interface StringProperty {
valueOf(): string;
[Symbol.toPrimitive] (h: 'default') : string;
}
How to make this work? Seems basic to me, but clearly I'm missing whatever the underlying issue is. But I'd like it to work... :)
I'm OK with all the extra TS declarations and annotations required to help ensure type safety and IDE hinting. Return on investment. But when it starts requiring actual code workarounds... that's too much IMHO.
Thank you for your consideration. -Max
BUMP!!!
@RyanCavanaugh could you briefly comment on @icholy's propostal described in this comment and implemented at this branch (not hacky at all) ? It seems like a sensible approach to me as it manages to handle Date. Date type is detected to be StrictOperand<string>
not StrictOperand<number>
.
My impression is that most wrapped primitive use-cases are number based as opposed to other primitive types. Considering this and also the fact that Date is quirky for addition, there is an even simpler apprach that only handles the number case. In this case, we just make sure to keep Date out: adding Dates would still not be allowed.
Kindly clarify what would be needed for such a proposal to be considered. Thank you.
It would be nice if an arithmetic operation could allow using the wrapped number because it's
valueOf()
method returns the expected primitive type.This can be generalized to: if type
T
is expected, then a value that implements the following interface can be used.