Skip to content

Commit a991880

Browse files
committed
types-grammar, ch4: adding 'To Primitive' discussion
1 parent 2d34934 commit a991880

File tree

1 file changed

+243
-6
lines changed

1 file changed

+243
-6
lines changed

types-grammar/ch4.md

Lines changed: 243 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Even values like `" "` (string with only whitespace), `[]` (empty array), and
7878

7979
| WARNING: |
8080
| :--- |
81-
| There *are* narrow, tricky exceptions to this truthy rule. For example, the web platform has deprecated the long-standing `document.all` collection/array feature, though it cannot be removed entirely -- that would break too many sites. Even where `document.all` is still defined, it behaves as a "falsy object" that coerces to `false`; that means legacy conditional checks like `if (document.all) { .. }` no longer pass. |
81+
| There *are* narrow, tricky exceptions to this truthy rule. For example, the web platform has deprecated the long-standing `document.all` collection/array feature, though it cannot be removed entirely -- that would break too many sites. Even where `document.all` is still defined, it behaves as a "falsy object"[^ExoticFalsyObjects] -- `undefined` which then coerces to `false`; this means legacy conditional checks like `if (document.all) { .. }` no longer pass. |
8282

8383
The `ToBoolean()` coercion operation is basically a lookup table rather than an algorithm of steps to use in coercions a non-boolean to a boolean. Thus, some developers assert that this isn't *really* coercion the way other abstract coercion operations are. I think that's bogus. `ToBoolean()` converts from non-boolean value-types to a boolean, and that's clear cut type coercion (even if it's a very simple lookup instead of an algorithm).
8484

@@ -96,7 +96,7 @@ ToPrimitive({ a: 1 },"string"); // "[object Object]"
9696
ToPrimitive({ a: 1 },"number"); // NaN
9797
```
9898

99-
The `ToPrimitive()` operation will look on the object provided, for either a `toString()` method or a `valueOf()` method; the order it looks for those is controlled by the *hint*.
99+
The `ToPrimitive()` operation will look on the object provided, for either a `toString()` method or a `valueOf()` method; the order it looks for those is controlled by the *hint*. `"string"` means check in `toString()` / `valueOf()` order, whereas `"number"` (or no *hint*) means check in `valueOf()` / `toString()` order.
100100

101101
If the method returns a value matching the *hinted* type, the operation is finished. But if the method doesn't return a value of the *hinted* type, `ToPrimitive()` will then look for and invoke the other method (if found).
102102

@@ -476,10 +476,6 @@ My take: `Boolean(..)` is the most preferable *explicit* coercion form. Further,
476476

477477
Since most developers, including famous names like Doug Crockford, also in practice use implicit (`boolean`) coercions in their `if`[^CrockfordIfs] and loop statements, I think we can say that at least *some forms* of *implicit* coercion are widely acceptable, regardless of the rhetoric to the contrary.
478478

479-
### To Primitive
480-
481-
// TODO
482-
483479
### To String
484480

485481
As with `ToBoolean()`, there are a number of ways to activate the `ToString()` coercion (as discussed earlier in the chapter). The decision of which approach is similarly subjective.
@@ -555,6 +551,10 @@ undefined + ""; // "undefined"
555551

556552
The `+ ""` idiom for string coercion takes advantage of the `+` overloading, without altering the final coerced string value. By the way, all of these work the same with the operands reversed (i.e., `"" + ..`).
557553

554+
| WARNING: |
555+
| :--- |
556+
| An extremely common misconception is that `String(x)` and `x + ""` are basically equivalent coercions, respectively just *explicit* vs *implicit* in form. But, that's not quite true! We'll revisit this in the "To Primitive" section later in this chapter. |
557+
558558
Some feel this is an *explicit* coercion, but I think it's clearly more *implicit*, in that it's taking advantage of the `+` overloading; further, the `""` is indirectly used to activate the coercion without modifying it. Moreover, consider what happens when this idiom is applied with a symbol value:
559559

560560
```js
@@ -573,6 +573,241 @@ Nevertheless, as I mentioned at the start of this chapter, Brendan Eich endorses
573573

574574
// TODO
575575

576+
### To Primitive
577+
578+
Most operators in JS, including those we've see with coercions to `string` and `number`, are designed to run against primitive values. When any of these operators is used instead against an object value, the abstract `ToPrimitive` algorithm (as described earlier) is activated to coerce the object to a primitive.
579+
580+
Let's set up an object we can use to inspect how different operations behave:
581+
582+
```js
583+
spyObject = {
584+
toString() {
585+
console.log("toString() invoked!");
586+
return "10";
587+
},
588+
valueOf() {
589+
console.log("valueOf() invoked!");
590+
return 42;
591+
},
592+
};
593+
```
594+
595+
This object defines both the `toString()` and `valueOf()` methods, and each one returns a different type of value (`string` vs `number`).
596+
597+
Let's try some of the coercion operations we've already seen:
598+
599+
```js
600+
String(spyObject);
601+
// toString() invoked!
602+
// "10"
603+
604+
spyObject + "";
605+
// valueOf() invoked!
606+
// "42"
607+
```
608+
609+
Whoa! I bet that surprised a few of you readers; it certainly did me. It's so common for people to assert that `String(..)` and `+ ""` are equivalent forms of activating the `ToString()` operation. But they're clearly not!
610+
611+
The difference comes down to the *hint* that each operation provides to `ToPrimitive()`. `String(..)` clearly provides `"string"` as the *hint*, whereas the `+ ""` idiom provides no *hint* (similar to *hinting* `"number"`). But don't miss this detail: even though `+ ""` invokes `valueOf()`, when that returns a `number` primitive value of `42`, that value is then coerced to a string (via `ToString()`), so we get `"42"` instead of `42`.
612+
613+
Let's keep going:
614+
615+
```js
616+
Number(spyObject);
617+
// valueOf() invoked!
618+
// 42
619+
620+
+spyObject;
621+
// valueOf() invoked!
622+
// 42
623+
```
624+
625+
This example implies that `Number(..)` and the unary `+` operator both perform the same `ToPrimitive()` coercion (with *hint* of `"number"`), which in our case returns `42`. Since that's already a `number` as requested, the value comes out without further ado.
626+
627+
But what if a `valueOf()` returns a `bigint`?
628+
629+
```js
630+
spyObject2 = {
631+
valueOf() {
632+
console.log("valueOf() invoked!");
633+
return 42n; // bigint!
634+
}
635+
};
636+
637+
Number(spyObject2);
638+
// valueOf() invoked!
639+
// 42 <--- look, not a bigint!
640+
641+
+spyObject2;
642+
// valueOf() invoked!
643+
// TypeError: Cannot convert a BigInt value to a number
644+
```
645+
646+
We saw this difference earlier in the "To Number" section. JS allows an *explicit* coercion of the `42n` bigint value to the `42` number value, but it disallows what it considers to be an *implicit* coercion form.
647+
648+
What about the `BigInt(..)` (no `new` keyword) coercion function?
649+
650+
```js
651+
BigInt(spyObject);
652+
// valueOf() invoked!
653+
// 42n <--- look, a bigint!
654+
655+
BigInt(spyObject2);
656+
// valueOf() invoked!
657+
// 42n
658+
659+
// *******************************
660+
661+
spyObject3 = {
662+
valueOf() {
663+
console.log("valueOf() invoked!");
664+
return 42.3;
665+
}
666+
};
667+
668+
BigInt(spyObject3);
669+
// valueOf() invoked!
670+
// RangeError: The number 42.3 cannot be converted to a BigInt
671+
```
672+
673+
Again, as we saw in the "To Number" section, `42` can safely be coerced to `42n`. On the other hand, `42.3` cannot safely be coerced to a `bigint`.
674+
675+
We've seen that `toString()` and `valueOf()` are invoked, variously, as certain `string` and `number` / `bigint` coercions are performed.
676+
677+
What about `boolean` coercions?
678+
679+
```js
680+
Boolean(spyObject);
681+
// true
682+
683+
!spyObject;
684+
// false
685+
686+
if (spyObject) {
687+
console.log("if!");
688+
}
689+
// if!
690+
691+
result = spyObject ? "ternary!" : "nope";
692+
// "ternary!"
693+
694+
while (spyObject) {
695+
console.log("while!");
696+
break;
697+
}
698+
// while!
699+
```
700+
701+
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.
702+
703+
#### Unboxing
704+
705+
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*.
706+
707+
Consider:
708+
709+
```js
710+
hello = new String("hello");
711+
String(hello); // "hello"
712+
hello + ""; // "hello"
713+
714+
fortyOne = new Number(41);
715+
Number(fortyOne); // 41
716+
fortyOne + 1; // 42
717+
```
718+
719+
The object wrappers `hello` and `fortyOne` above have `toString()` and `valueOf()` methods configured on them, to behave similarly to the `spyObject` / etc objects from our previous examples.
720+
721+
A special case to be careful of with wrapped-object primitives is with `Boolean()`:
722+
723+
```js
724+
nope = new Boolean(false);
725+
Boolean(nope); // true <--- oops!
726+
!!nope; // true <--- oops!
727+
```
728+
729+
Remember, this is because `ToBoolean()` does *not* reduce an object to its primitive form with `ToPrimitive`; it merely looks up the value in its internal table, and since normal (non-exotic[^ExoticFalsyObjects]) objects are always truthy, `true` comes out.
730+
731+
| NOTE: |
732+
| :--- |
733+
| It's a nasty little gotcha. A case could certainly be made that `new Boolean(false)` should configure itself internally as an exotic "falsy object". [^ExoticFalsyObjects] Unfortunately, that change now, 25 years into JS's history, could easily create breakage in programs. As such, JS has left this gotcha untouched. |
734+
735+
#### Overriding Default `toString()`
736+
737+
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`:
738+
739+
```js
740+
spyObject4a = {};
741+
String(spyObject4a);
742+
// "[object Object]"
743+
spyObject4a.toString();
744+
// "[object Object]"
745+
746+
spyObject4b = {
747+
[Symbol.toStringTag]: "my-spy-object"
748+
};
749+
String(spyObject4b);
750+
// "[object my-spy-object]"
751+
spyObject4b.toString();
752+
// "[object my-spy-object]"
753+
754+
spyObject4c = {
755+
get [Symbol.toStringTag]() {
756+
return `myValue:${this.myValue}`;
757+
},
758+
myValue: 42
759+
};
760+
String(spyObject4c);
761+
// "[object myValue:42]"
762+
spyObject4c.toString();
763+
// "[object myValue:42]"
764+
```
765+
766+
`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.
767+
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.
769+
770+
#### Overriding `ToPrimitive`
771+
772+
You can alternately override the whole default `ToPrimitive()` operation for any object, by setting the special symbol property `Symbol.toPrimitive` to hold a function:
773+
774+
```js
775+
spyObject5 = {
776+
[Symbol.toPrimitive](hint) {
777+
console.log(`toPrimitive(${hint}) invoked!`);
778+
return 25;
779+
},
780+
toString() {
781+
console.log("toString() invoked!");
782+
return "10";
783+
},
784+
valueOf() {
785+
console.log("valueOf() invoked!");
786+
return 42;
787+
},
788+
};
789+
790+
String(spyObject5);
791+
// toPrimitive(string) invoked!
792+
// "25" <--- not "10"
793+
794+
spyObject5 + "";
795+
// toPrimitive(default) invoked!
796+
// "25" <--- not "42"
797+
798+
Number(spyObject5);
799+
// toPrimitive(number) invoked!
800+
// 25 <--- not 42 or "25"
801+
802+
+spyObject5;
803+
// toPrimitive(number) invoked!
804+
// 25
805+
```
806+
807+
As you can see, if you define this function on an object, it's used entirely in replacement of the default `ToPrimitive()` abstract operation. Since `hint` is still provided to this invoked function (`[Symbol.toPrimitive](..)`), you could in theory implement your own version of the algorithm, invoking a `toString()`, `valueOf()`, or any other method on the object (`this` context reference).
808+
809+
Or you can just manually define a return value as shown above. Regardless, JS will *not* automatically invoke either `toString()` or `valueOf()` methods.
810+
576811
### Nullish
577812

578813
// TODO
@@ -589,6 +824,8 @@ Nevertheless, as I mentioned at the start of this chapter, Brendan Eich endorses
589824

590825
[^ToBoolean]: "7.1.2 ToBoolean(argument)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-toboolean ; Accessed August 2022
591826

827+
[^ExoticFalsyObjects]: "B.3.6 The [[IsHTMLDDA]] Internal Slot", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-IsHTMLDDA-internal-slot ; Accessed August 2022
828+
592829
[^OrdinaryToPrimitive]: "7.1.1.1 OrdinaryToPrimitive(O,hint)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-ordinarytoprimitive ; Accessed August 2022
593830

594831
[^ToString]: "7.1.17 ToString(argument)", ECMAScript 2022 Language Specification; https://262.ecma-international.org/13.0/#sec-tostring ; Accessed August 2022

0 commit comments

Comments
 (0)