Skip to content
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

Allow wrapped values to be used in place of primitives. #2361

Open
icholy opened this issue Mar 14, 2015 · 42 comments
Open

Allow wrapped values to be used in place of primitives. #2361

icholy opened this issue Mar 14, 2015 · 42 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@icholy
Copy link

icholy commented Mar 14, 2015

class NumberWrapper {
    constructor(private value: number) {}
    valueOf(): number { return this.value; }
}

var x = new NumberWrapper(1);

// The right-hand side of an arithmetic operation 
// must be of type 'any', 'number' or an enum type.
console.log(2 + x);

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.

interface Wrapped<T> {
    valueOf(): T;
}
@icholy icholy changed the title Use valueOf for arithmetic operations. Allow wrapped values to be used in place of primitives. valueOf() Mar 16, 2015
@icholy icholy changed the title Allow wrapped values to be used in place of primitives. valueOf() Allow wrapped values to be used in place of primitives. Mar 16, 2015
@danquirk danquirk added the Suggestion An idea for TypeScript label Mar 16, 2015
@danquirk
Copy link
Member

What specific use cases do you have in mind here?

@icholy
Copy link
Author

icholy commented Mar 16, 2015

@danquirk this

@RyanCavanaugh RyanCavanaugh added the In Discussion Not yet reached consensus label Mar 17, 2015
@RyanCavanaugh
Copy link
Member

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.

@icholy
Copy link
Author

icholy commented Mar 17, 2015

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?

@RyanCavanaugh
Copy link
Member

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)"

😢

@icholy
Copy link
Author

icholy commented Mar 17, 2015

Oh man ... I didn't know that.

@icholy
Copy link
Author

icholy commented Mar 18, 2015

What if Date was treated as a special case? Either don't allow it, or have the compiler yell at you.

@RyanCavanaugh RyanCavanaugh added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. and removed In Discussion Not yet reached consensus labels May 4, 2015
@RyanCavanaugh
Copy link
Member

Would like to see this as e.g. a change to the spec + implementation that specifies exactly how this might work

@Zorgatone
Copy link

I need this one 👍

@demurgos
Copy link

demurgos commented Apr 29, 2016

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.

@icholy
Copy link
Author

icholy commented Feb 3, 2017

@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>);

4.19.1 The *, /, %, –, <<, >>, >>>, &, ^, and | operators

These operators require their operands to be of type Any, the Operand type, or an enum type. Operands of an enum type are treated as having the primitive type Number. If one operand is the null 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

4.19.2 The + operator

The binary + operator requires both operands to be of the StrictOperand type or an enum type, or at least one of the operands to be of type Any or a Operand type. Operands of an enum type are treated as having the primitive type Number. If one operand is the null 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

@icholy
Copy link
Author

icholy commented Feb 3, 2017

@RyanCavanaugh I did (an extremely hacky) implementation and it does seem to work master...icholy:master

@icholy
Copy link
Author

icholy commented Feb 6, 2017

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();
    }
  }
}

@GCastilho
Copy link

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 ", because it's 7 years already, letting this without even a resolution with clearly a lot of people considering this as a bug it's a bit absurd

@niedzielski
Copy link

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

@Llorx
Copy link

Llorx commented Sep 12, 2022

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)))

@mike-lischke
Copy link

mike-lischke commented Dec 29, 2022

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

@dotnetCarpenter
Copy link

dotnetCarpenter commented Dec 29, 2022

@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 of new. ~@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)" 

Playground Link

@aradalvand
Copy link

aradalvand commented Mar 11, 2023

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.

@RyanCavanaugh
Copy link
Member

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.

@CryptoCrocodile
Copy link

Also a +1 for this. The compiler should recognize the presence of valueOf and allow its usage as per the JS standard.

@rkrisztian
Copy link

rkrisztian commented Nov 8, 2023

I agree that JavaScript works in weird ways (#2361 (comment)), but Date is definitely not the only problem JavaScript has, and TypeScript is supposed to be a superset of JavaScript (#2361 (comment)). 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.)

@mpaperno
Copy link

mpaperno commented Jan 2, 2024

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

elle-j added a commit to realm/realm-js that referenced this issue May 31, 2024
elle-j added a commit to realm/realm-js that referenced this issue Jun 12, 2024
elle-j added a commit to realm/realm-js that referenced this issue Jun 12, 2024
…pe (#6694)

* Group schema normalization tests into suites.

* Allow 'counter' in schema parser.

* Test schema parser with 'counter'.

* Add Counter class.

* Implement creating a counter on Realm object.

* Add implementation to Counter.

* Expose Core's 'add_int()'.

* Add object property getter.

* Export types.

* Test create and access via 'realm.create()' (input: nums and Counter).

* Test create and access via 'realm.create()' (input: collections).

* Test create and access via collection accessors.

* Test updating the counter value.

* Test updating the Realm object counter property.

* Move common logic to shared functions.

* Test updating the Realm object collection property.

* Test returning different reference for each access.

* Test throwing when updating by non-integer.

* Change how a counter should be defined in the schema.

We originally decided on using 'counter' as its own primitive type (defined e.g. "type: 'counter'"). We have updated this to now use a 'presentation' type (e.g. "type: 'int', presentation: 'counter'").

* Rename test property.

* Add 'skips' to tests that await implementation.

* Add 'presentations' to the stored sdk-specifc schema info.

* Add 'isCreating' argument to object property setters to disallow certain operations for counter.

* Test throwing when resetting via property setter.

* Test throwing outside transaction.

* Test throwing if setting regular int to counter.

* Test throwing if invalidated object.

* Extend 'Unmanaged' with Counter and allowing number.

* Throw custom message if getting value on invalid object.

* Implement 'Counter.set()'.

* Disallow counter as mixed.

* Test throwing if used as mixed.

* Test filtering.

* Fixing returning null if counter is null.

* Remove support for collections of counters.

* Test updating Realm object counter prop via UpdateMode.

* Fix Unmanaged type for Counter.

* Add Unmanaged RealmSet.

* Update API docs.

* Remove implicit coercion to number due to TS not supporting it.

For reference on lack of TS support, see: microsoft/TypeScript#2361

* Test 'Realm.schema'.

* Replace 'Object.defineProperty' with Symbols.

* Add CHANGELOG entry.

* Change default value in test to non-zero.

* Test throwing when setting 'Counter.value' directly.

* Make 'describe' test names lowercase.

* List what is not supported onto the 'Counter' API docs.

* Move 'expectCounter' to common file.

* Remove link to API ref docs in CHANGELOG if doing RC release.

* Refactor a test to use `map` instead of for-loop

Co-authored-by: Kræn Hansen <kraen.hansen@mongodb.com>

* Update error message from 'floating point' to 'decimal'.

* [12.10.0-rc.0] Bump version (#6696)

Co-authored-by: elle-j <elle-j@users.noreply.github.com>

* Prepare for vNext (#6697)

Co-authored-by: elle-j <elle-j@users.noreply.github.com>

* Update formatting.

* Update test variable name.

* Add links in CHANGELOG.

Co-authored-by: Kenneth Geisshirt <kenneth.geisshirt@mongodb.com>

* Update API docs formatting.

* Update initial value in test from 0 to 10.

* Refactor similar tests into for-loop.

* Point to Core v14.10.0.

* Update Counter API docs.

* Remove redundant comment.

* Refactor logic allowing/disallowing setting a counter.

* Update type name w/o abbreviation

Co-authored-by: Kræn Hansen <kraen.hansen@mongodb.com>

* Fix CHANGELOG after merge.

---------

Co-authored-by: Kræn Hansen <kraen.hansen@mongodb.com>
Co-authored-by: Yavor Georgiev <fealebenpae@users.noreply.github.com>
Co-authored-by: elle-j <elle-j@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Kenneth Geisshirt <kenneth.geisshirt@mongodb.com>
@daxliniere

This comment was marked as off-topic.

@pedropedruzzi
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests