Skip to content

Commit 6f2cfc1

Browse files
committed
types-grammar, ch4: adding 'To Number' discussion, as well as some more details to 'To Primitive' section
1 parent a991880 commit 6f2cfc1

File tree

1 file changed

+142
-18
lines changed

1 file changed

+142
-18
lines changed

types-grammar/ch4.md

Lines changed: 142 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,96 @@ Nevertheless, as I mentioned at the start of this chapter, Brendan Eich endorses
571571

572572
### To Number
573573

574-
// TODO
574+
Numeric coercions are a bit more complicated than string coercions, since we can be talking about either `number` or `bigint` as the target type. There's also a much smaller set of values that can be validly represented numerically (everything else becomes `NaN`).
575+
576+
Let's start with the `Number(..)` and `BigInt(..)` functions (no `new` keywords):
577+
578+
```js
579+
Number("42"); // 42
580+
Number("-3.141596"); // -3.141596
581+
Number("-0"); // -0
582+
583+
BigInt("42"); // 42n
584+
BigInt("-0"); // 0n
585+
```
586+
587+
`Number` coercion which fails (not recognized) results in `NaN` (see "Invalid Number" in Chapter 1), whereas `BigInt` throws an exception:
588+
589+
```js
590+
Number("123px"); // NaN
591+
592+
BigInt("123px");
593+
// SyntaxError: Cannot convert 123px to a BigInt
594+
```
595+
596+
Moreover, even though `42n` is valid syntax as a literal `bigint`, the string `"42n"` is never a recognized string representation of a `bigint`, by either of the coercive function forms:
597+
598+
```js
599+
Number("42n"); // NaN
600+
601+
BigInt("42n");
602+
// SyntaxError: Cannot convert 42n to a BigInt
603+
```
604+
605+
However, we *can* coerce numeric strings with other representations of the numbers than typical base-10 decimals (see Chapter 1 for more information):
606+
607+
```js
608+
Number("0b101010"); // 42
609+
610+
BigInt("0b101010"); // 42n
611+
```
612+
613+
Typically, `Number(..)` and `BigInt(..)` receive string values, but that's not actually required. For example, `true` and `false` coerce to their typical numeric equivalents:
614+
615+
```js
616+
Number(true); // 1
617+
Number(false); // 0
618+
619+
BigInt(true); // 1n
620+
BigInt(false); // 0n
621+
```
622+
623+
You can also generally coerce between `number` and `bigint` types:
624+
625+
```js
626+
Number(42n); // 42
627+
Number(42n ** 1000n); // Infinity
628+
629+
BigInt(42); // 42n
630+
```
631+
632+
We can also use the `+` unary operator, which is commonly assumed to coerce the same as the `Number(..)` function:
633+
634+
```js
635+
+"42"; // 42
636+
+"0b101010"; // 42
637+
```
638+
639+
Be careful though. If the coercions are unsafe/invalid in certain ways, exceptions are thrown:
640+
641+
```js
642+
BigInt(3.141596);
643+
// RangeError: The number 3.141596 cannot be converted to a BigInt
644+
645+
+42n;
646+
// TypeError: Cannot convert a BigInt value to a number
647+
```
648+
649+
Clearly, `3.141596` does not safely coerce to an integer, let alone a `bigint`.
650+
651+
But `+42n` throwing an exception is an interesting case. By contrast, `Number(42n)` works fine, so it's a bit surprising that `+42n` fails.
652+
653+
| WARNING: |
654+
| :--- |
655+
| That surprise is especially palpable since prepending a `+` in front of a number is typically assumed to just mean a "positive number", the same way `-` in front a number is assumed to mean a "negative number". As explained in Chapter 1, however, JS numeric syntax (`number` and `bigint`) recognize no syntax for "negative values". All numeric literals are parsed as "positive" by default. If a `+` or `-` is prepended, those are treated as unary operators applied against the parsed (positive) number. |
656+
657+
OK, so `+42n` is parsed as `+(42n)`. But still... why is `+` throwing an exception here?
658+
659+
You might recall earlier when we showed that JS allows *explicit* string coercion of symbol values, but disallows *implicit* string coercions? The same thing is going on here. JS language design interprets unary `+` in front of a `bigint` value as an *implicit* `ToNumber()` coercion (thus disallowed!), but `Number(..)` is interpreted as an *explicit* `ToNumber()` coercion (thus allowed!).
660+
661+
In other words, contrary to popular assumption/assertion, `Number(..)` and `+` are not interchangable. I think `Number(..)` is the safer/more reliable form.
662+
663+
Like string coercions, if you perform a numeric coercion on a non-primitive object value, the `ToPrimitive()` operation is activated to first turn it into some primitive value
575664

576665
### To Primitive
577666

@@ -674,7 +763,38 @@ Again, as we saw in the "To Number" section, `42` can safely be coerced to `42n`
674763

675764
We've seen that `toString()` and `valueOf()` are invoked, variously, as certain `string` and `number` / `bigint` coercions are performed.
676765

677-
What about `boolean` coercions?
766+
#### No Primitive Found?
767+
768+
If `ToPrimitive()` fails to produce a primitive value, an exception will be thrown:
769+
770+
```js
771+
spyObject4 = {
772+
toString() {
773+
console.log("toString() invoked!");
774+
return [];
775+
},
776+
valueOf() {
777+
console.log("valueOf() invoked!");
778+
return {};
779+
}
780+
};
781+
782+
String(spyObject4);
783+
// toString() invoked!
784+
// valueOf() invoked!
785+
// TypeError: Cannot convert object to primitive value
786+
787+
Number(spyObject4);
788+
// valueOf() invoked!
789+
// toString() invoked!
790+
// TypeError: Cannot convert object to primitive value
791+
```
792+
793+
If you're going to define custom to-primitive coercions via `toString()` / `valueOf()`, make sure to return a primitive from at least one of them!
794+
795+
#### Object To Boolean
796+
797+
What about `boolean` coercions of objects?
678798

679799
```js
680800
Boolean(spyObject);
@@ -700,7 +820,7 @@ while (spyObject) {
700820

701821
Each of these are activating `ToBoolean()`. But if you recall from earlier, *that* algorithm never delegates to `ToPrimitive()`; thus, we don't see "valueOf() invoked!" being logged out.
702822

703-
#### Unboxing
823+
#### Unboxing: Wrapper To Primitive
704824

705825
A special form of objects that are often `ToPrimitive()` coerced: boxed/wrapped primitives (as seen in Chapter 3). This particular object-to-primitive coercion is often referred to as *unboxing*.
706826

@@ -737,42 +857,42 @@ Remember, this is because `ToBoolean()` does *not* reduce an object to its primi
737857
As we've seen, you can always define a `toString()` on an object to have *it* invoked by the appropriate `ToPrimitive()` coercion. But another option is to override the `Symbol.toStringTag`:
738858

739859
```js
740-
spyObject4a = {};
741-
String(spyObject4a);
860+
spyObject5a = {};
861+
String(spyObject5a);
742862
// "[object Object]"
743-
spyObject4a.toString();
863+
spyObject5a.toString();
744864
// "[object Object]"
745865

746-
spyObject4b = {
866+
spyObject5b = {
747867
[Symbol.toStringTag]: "my-spy-object"
748868
};
749-
String(spyObject4b);
869+
String(spyObject5b);
750870
// "[object my-spy-object]"
751-
spyObject4b.toString();
871+
spyObject5b.toString();
752872
// "[object my-spy-object]"
753873

754-
spyObject4c = {
874+
spyObject5c = {
755875
get [Symbol.toStringTag]() {
756876
return `myValue:${this.myValue}`;
757877
},
758878
myValue: 42
759879
};
760-
String(spyObject4c);
880+
String(spyObject5c);
761881
// "[object myValue:42]"
762-
spyObject4c.toString();
882+
spyObject5c.toString();
763883
// "[object myValue:42]"
764884
```
765885

766886
`Symbol.toStringTag` is intended to define a custom string value to describe the object whenever its default `toString()` operation is invoked directly, or implicitly via coercion; in its absence, the value used is `"Object"` in the common `"[object Object]"` output.
767887

768-
The `get ..` syntax in `spyObject4c` is defining a *getter*. That means when JS tries to access this `Symbol.toStringTag` as a property (as normal), this gettter code instead causes the function we specify to be invoked to compute the result. We can run any arbitrary logic inside this getter to dynamically determine a string *tag* for use by the default `toString()` method.
888+
The `get ..` syntax in `spyObject5c` is defining a *getter*. That means when JS tries to access this `Symbol.toStringTag` as a property (as normal), this gettter code instead causes the function we specify to be invoked to compute the result. We can run any arbitrary logic inside this getter to dynamically determine a string *tag* for use by the default `toString()` method.
769889

770890
#### Overriding `ToPrimitive`
771891

772892
You can alternately override the whole default `ToPrimitive()` operation for any object, by setting the special symbol property `Symbol.toPrimitive` to hold a function:
773893

774894
```js
775-
spyObject5 = {
895+
spyObject6 = {
776896
[Symbol.toPrimitive](hint) {
777897
console.log(`toPrimitive(${hint}) invoked!`);
778898
return 25;
@@ -787,19 +907,19 @@ spyObject5 = {
787907
},
788908
};
789909

790-
String(spyObject5);
910+
String(spyObject6);
791911
// toPrimitive(string) invoked!
792912
// "25" <--- not "10"
793913

794-
spyObject5 + "";
914+
spyObject6 + "";
795915
// toPrimitive(default) invoked!
796916
// "25" <--- not "42"
797917

798-
Number(spyObject5);
918+
Number(spyObject6);
799919
// toPrimitive(number) invoked!
800920
// 25 <--- not 42 or "25"
801921

802-
+spyObject5;
922+
+spyObject6;
803923
// toPrimitive(number) invoked!
804924
// 25
805925
```
@@ -808,6 +928,10 @@ As you can see, if you define this function on an object, it's used entirely in
808928

809929
Or you can just manually define a return value as shown above. Regardless, JS will *not* automatically invoke either `toString()` or `valueOf()` methods.
810930

931+
| WARNING: |
932+
| :--- |
933+
| As discussed prior in "No Primitive Found?", if the defined `Symbol.toPrimitive` function does not actually return a value that's a primitive, an exception will be thrown about being unable to "...convert object to primitive value". Make sure to always return an actual primitive value from such a function! |
934+
811935
### Nullish
812936

813937
// TODO

0 commit comments

Comments
 (0)