The TC39 Decimal proposal aims to add exact decimal arithmetic to JavaScript, eliminating the rounding errors that occur with binary floating-point numbers.
There are two ways to see why this matters. The practical one is that end-user-visible bugs come from using binary floating-point Numbers for calculations that are intended to be in decimal arithmetic. The other is more foundational: we want a numeric type that behaves the way we were taught arithmetic works: 0.1 + 0.2 is 0.3. Decimal is, in essence, a do-what-I-mean (DWIM) approach to numbers: the value and the everyday mental model line up, with no hidden binary representation oddities.
The champions welcome your participation in discussing the design space in the issues linked above. We are seeking input for your needs around JavaScript decimal in this survey.
Champions:
- Jesse Alama (Igalia)
- Jirka Maršík (Oracle)
- Andrew Paprocki (Bloomberg)
Authors: Jesse Alama, Waldemar Horwat
Stage: Stage 1 of the TC39 process. A draft specification is available.
Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript. Currently, developers sometimes represent these using libraries for this purpose, or sometimes use Strings. Sadly, JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding errors.
What’s the issue? Why doesn't 0.1 + 0.2 work out to 0.3? In what sense are JS Numbers not “exact”? How is it possible that JavaScript's Numbers get something wrong, and have been getting it wrong for so long?
As currently defined in JavaScript, Numbers are 64-bit binary floating-point numbers. The conversion from most decimal values to binary floats rarely is an exact match. For instance: the decimal number 0.5 can be exactly represented in binary, but not 0.1; in fact, the 64-bit floating point number corresponding to 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. Same for 0.2, 0.3, … Statistically, most human-authored decimal numbers cannot be exactly represented as a binary floating-point number (AKA float).
The goal of the Decimal proposal is to add support to the JavaScript standard library for decimal numbers in a way that provides good ergonomics and functionality. JS programmers should feel comfortable using decimal numbers, when those are appropriate.
Many currencies tend to be expressed with decimal quantities. Although it’s possible to represent money as integer “cents” (multiply all quantities by 100), this approach runs into a couple of issues:
- There’s a persistent mismatch between the way humans think about money and the way it’s manipulated in the program, causing mental overhead for the programmer aware of the issue.
- Some programmers may not even be aware of this mismatch. This opens the door to rounding errors whose source is unknown. If calculations start to get more involved, the chance of error increases.
- Different currencies use different numbers of decimal positions which is easy to get confused; the hack of working with quantities that are implicitly multiplied by 100 may not work when working with multiple currencies. For instance, it's not correct to assume that all currencies have two decimal places, or that the only exception is JPY (Japanese yen); making such assumptions will make it hard to internationalize code to new countries.
- Sometimes, fractional cents need to be represented too (e.g., as precise prices that occur, for instance, in stock trading or currency conversion).
In the examples that follow, we'll use Decimal objects.
function calculateBill(items, tax) {
let total = new Decimal(0);
for (let { price, count } of items) {
total = total.add(new Decimal(price).times(new Decimal(count)));
}
return total.multiply(tax.add(new Decimal(1)));
}
let items = [
{ price: "1.25", count: 5 },
{ price: "5.00", count: 1 },
];
let tax = new Decimal("0.0735");
let total = calculateBill(items, tax);
console.log(total.toFixed(2));Let's convert USD to EUR, given the exchange rate EUR --> USD. Decimal does the exact arithmetic; we then hand the result to Amount to round it for display, attach the currency, and localize it.
// Exact arithmetic is Decimal's job:
let exchangeRateEurToUsd = new Decimal("1.09");
let amountInUsd = new Decimal("450.27");
let exchangeRateUsdToEur = new Decimal("1").divide(exchangeRateEurToUsd);
let amountInEur = amountInUsd.multiply(exchangeRateUsdToEur);
// Presentation is Amount's job (precision + currency + locale):
let display = new Amount(amountInEur.toString(), {
unit: "EUR",
fractionDigits: 2,
});
console.log(display.toLocaleString("de-DE")); // "413,09 €"Today the two types are bridged with toString(); a natural point of coordination between the proposals is for Amount to accept a Decimal value directly, so the handoff becomes new Amount(amountInEur, { unit: "EUR", fractionDigits: 2 }).
Historically, JavaScript may not have been considered a language where exact decimal numbers are even exactly representable, with the understanding that doing calculations is bound to propagate any initial rounding errors when numbers were created. In some application architectures, JS only deals with a string representing a human-readable decimal quantity (e.g, "1.25"), and never does calculations or conversions. However, several trends push towards JS’s deeper involvement in with decimal quantities:
- More complicated frontend architectures: Rounding, localization or other presentational aspects may be performed on the frontend for better interactive performance.
- Serverless: Many Serverless systems use JavaScript as a programming language in order to better leverage the knowledge of frontend engineers.
- Server-side programming in JavaScript: Systems like Node.js and Deno have grown in popularity to do more traditional server-side programming in JavaScript.
In all of these environments, the lack of decimal number support means that various workarounds have to be used (assuming, again, that programmers are even aware of the inherent mismatch between JS’s built-in binary floating-point numbers and proper decimal numbers):
- An external library could be used instead (introducing issues about choosing the library, coordinating on its use).
- Calculations could be in terms of “cents” (fallible, as explained above)
- In some cases, developers end up using Number anyway—aware of its limitations or believing it to be mostly safe—but in practice cause bugs, even when they try to take care of rounding or non-exact conversions from decimals to binary floats.
In other words, with JS increasingly being used in contexts and scenarios where it traditionally did not appear, the need for being able to natively handle basic data, such as decimal numbers, that other systems already natively handle is increasing.
JavaScript is often the glue between systems—databases, foreign-function interfaces, other services—that natively support decimal numbers. Decimal lets you take that data and compute on it exactly, preserving its mathematical value.
Note that Decimal preserves value, not representation: a Decimal built from "1.20" is indistinguishable from one built from "1.2" (see Data model). So if you only need to transport a value untouched, a String already does that losslessly. If you need to carry a declared precision or scale across the boundary, that is the job of Amount. Decimal earns its place once you need to calculate and preserve mathematical value.
The following example illustrates the idea. Note the sql_decimal configuration option, and how values returned from the DB arrive as Decimal values—ready for exact arithmetic—rather than as strings or JS Numbers:
const { Client } = require("pg");
const client = new Client({
user: "username",
sql_decimal: "decimal", // or 'string', 'number'
// ...more options
});
const boost = new Decimal("1.05");
client.query("SELECT prices FROM data_with_numbers", (err, res) => {
if (err) throw err;
console.log(res.rows.map((row) => row.prices.times(boost)));
client.end();
});This use case implies the following goals:
- Exact arithmetic: Addition, subtraction, multiplication, and division compute their exact decimal result, with no unintended rounding to cause user-visible errors
- Explicit, opt-in rounding when a calculation requires it, with a choice of rounding mode
- Sufficient precision for typical human-readable quantities, preserved across arithmetic
- Sufficient ergonomics to enable correct usage
- Be implementable with adequate performance/memory usage for applications
- (Please file an issue to mention more requirements)
In addition to the goals which come directly from the use case mentioned above:
- Well-defined semantics, with the same result regardless of which implementation and context a piece of code is run in
- Build a consistent story for numerics in JavaScript together with Numbers, BigInt, operator overloading, and potential future built-in numeric types
- No global mutable state involved in operator semantics; dynamically scoped state also discouraged
- Ability to be implemented across all JavaScript environment (e.g., embedded, server, browser)
If Decimal becomes a part of standard JavaScript, it may be used in some built-in APIs in host environments:
- For the Web platform:
(#4)
- HTML serialization would support Decimal, just as it supports BigInt, so Decimal could be used in
postMessage,IndexedDB, etc.
- HTML serialization would support Decimal, just as it supports BigInt, so Decimal could be used in
- For WebAssembly, if WebAssembly adds IEEE 64-bit and/or 128-bit decimal scalar types some day, then the WebAssembly/JS API could introduce conversions along the boundary, analogous to WebAssembly BigInt/i64 integration
More host API interactions are discussed in #5.
Adding decimal arithmetic would not be an instance of JS breaking new ground. Many major programming languages have recognized that decimal arithmetic is fundamental enough to include in the standard library, if not as a primitive type.
| Language | Type Name | Location | Year Added | Notes |
|---|---|---|---|---|
| Python | decimal.Decimal |
Standard library | 2003 (Python 2.3) | Based on General Decimal Arithmetic Specification |
| Java | java.math.BigDecimal |
Standard library | 1998 (JDK 1.1) | Arbitrary precision, widely used for financial calculations |
| C# | decimal |
Primitive structure type | 2000 (C# 1.0) | 128-bit decimal type, first-class language support |
| Swift | Decimal (formerly NSDecimalNumber) |
Foundation framework | 2016 (Swift 3.0) | Standard library type for financial calculations |
| Ruby | BigDecimal |
Standard library | 1999 (Ruby 1.6) | Arbitrary precision decimal arithmetic |
SQL also has NUMERIC/DECIMAL as a built-in type, predating many programming languages.
Many languages added decimal support 10+ years ago. The IEEE 754-2008 standard for Decimal128 is now 17 years old. (The 2019 edition of IEEE 754 also has decimal arithmetic.)
Moreover, having multiple numeric types in a language is not unusual. Languages ship with integers, floats, and decimals. (Some even have more numbers than that, such as built-in support for rationals.) Python even includes both decimals and rationals. The existence of JS's Number and BigInt doesn't preclude Decimal. Moreover, the need for decimal types across many languages reflects a genuine, universal need rather than a niche requirement.
The table above is deliberately short. Decimal, rationals, and similar features are in fact pervasive across standards, programming languages, databases, libraries, and even hardware. Below is a partial catalog, with a brief summary of the semantics in each.
- Standards
- IEEE 754-2008 and later: 32-bit, 64-bit and 128-bit decimal; see explanation (recommends against using 32-bit decimal)
- (plus many of the below are standardized)
- Programming languages
- Fixed-size decimals:
- C: 32, 64 and 128-bit IEE 754 decimal types, with a global settings object. Still a proposal, but has a GCC implementation.
- C++: Early proposal work in progress, to be based on IEEE 64 and 128-bit decimal. Still a proposal, but has a GCC implementation.
- C#/.NET: Custom 128-bit decimal semantics with slightly different sizes for the mantissa vs exponent compared to IEEE.
- Swift/Obj-C: Yet another custom semantics for fixed-bit-size floating point decimal.
- Global settings for setting decimal precision
- Python: Decimal with global settings to set precision.
- Rationals
- Perl6: Literals like
1.5are Rat instances! - Common Lisp: Ratios live alongside floats; no decimal data type
- Scheme: Analogous to Common Lisp, with different names for types (Racket is similar)
- Ruby: Rational class alongside BigDecimal.
- Perl6: Literals like
- Arbitrary-precision decimals (this proposal)
- Ruby: Arbitrary-precision Decimal, alongside Rational.
- PHP: A set of functions to bind to bc for mathematical calculations. An alternative community-driven Decimal library is also available.
- Java: Arbitrary-precision decimal based on objects and methods. Requires rounding modes and precision parameters for operations like division
- Fixed-size decimals:
- Databases
- Decimal with precision configurable in the schema
- IEEE 754-2008 decimal
- Libraries
- Hardware (all implementing IEEE decimal)
JavaScript's origins as a lightweight browser scripting language meant it initially shipped with minimal numeric support (just IEEE 754 binary floats). However, JavaScript's role has fundamentally changed. Back in the 1990s, JS was focused on form validation and DOM manipulation. Now we have full-stack applications, including financial systems, e-commerce platforms, serverless backends, and data processing pipelines. The language has evolved to meet modern needs but decimal arithmetic remains an important gap.
Because JavaScript lacks native decimal support, developers have created numerous userland solutions:
- decimal.js: ~2M weekly npm downloads
- bignumber.js: ~800K weekly npm downloads
- big.js: ~500K weekly npm downloads
There are also money-specific libraries such as dinero.js with about 180K weekly NPM downloads. Each implementation has slightly different semantics, requires coordination across libraries, adds to total bundle size, presumably performs worse than native implementations could, and creates interoperability challenges. We believe Decimal will standardize existing practice: developers are already reaching for these libraries, or falling back on the error-prone workarounds described above. Decimal doesn't ask developers to adopt something new; it offers a better way to do what they're already struggling to do.
The three most popular libraries above are each by MikeMcl. They have some interesting differences, but also share a common shape: an object-and-method API, with rounding modes and precision limits configured on the constructor, and support for inherently rounding operations like square root, exponentiation, and division. We plan to learn from developers' experiences with these and other ecosystem libraries as we refine Decimal's design; the discussion continues in #22.
Based on feedback from JS developers, engine implementors, and the members of the TC39 committee, we have a concrete proposal. Please see the spec text (rendered HTML). You’re encouraged to join the discussion by commenting on the issues linked below or filing your own.
We will use the Decimal128 data model for JavaScript decimals. Decimal128 is not a new standard; it was added to the IEEE 754 floating-point arithmetic standard in 2008 (and is present in the 2019 edition of IEEE 754, which JS normatively depends upon). It represents the culmination of decades of research on decimal floating-point numbers. Values in the Decimal128 universe take up 128 bits. In this representation, up to 34 significant digits (that is, decimal digits) can be stored, with an exponent (power of ten) of +/- 6143.
The "BigDecimal" data model is based on unlimited-size decimals (no fixed bith-width), understood exactly as mathematical values.
From the champion group’s perspective, both BigDecimal and Decimal128 are both coherent, valid proposals that would meet the needs of the main use case. Just looking at the diversity of semantics in other programming languages, and the lack of practical issues that programmers run into, shows us that there are many workable answers here.
Operators always calculate their exact answer. In particular, if two BigDecimals are multiplied, the precision of the result may be up to the sum of the operands. For this reason, BigDecimal.pow takes a mandatory options object, to ensure that the result does not go out of control in precision.
One can conceive of an arbitrary-precision version of decimals, and we have explored that route; historical information is available at bigdecimal-reference.md.
One difficulty with BigDecimal is that division is not available as a two-argument function because a rounding parameter is, in general, required. A BigDecimal.div function would be needed, where some options would be mandatory. See #13 for further discussion of division in BigDecimal.
Further discussion of the tradeoffs between BigDecimal and Decimal128 can be found in #8.
Imagine that every decimal number has, say, ten digits after the decimal point. Anything requiring, say, eleven digits after the decimal point would be unrepresentable. This is the world of fixed-precision decimals. The number ten is just an example; some research would be required to find out what a good number would be. One could even imagine that the precision of such numbers could be parameterized.
Rational numbers, AKA fractions, offer an adjacent approach to decimals. From a mathematical point of view, rationals are more expressive than decimals: every decimal is a kind of fraction (a signed integer divided by a power of ten), whereas some rationals, such as 1/3, cannot be (finitely) represented as decimals. So why not rationals?
- The size of the numerators and denominators, in general, grows exponentially as one carries out operations. Performing just one multiplication or division will in general cause the size of the parts of the rational to be multiplied. Even addition and subtraction cause rapid growth. This means that a heavy cost is paid for the precision offered by rationals.
- One must be vigilant about normalization of numerators and denominators, which involves repeatedly computing GCDs, dividing numerator and denominator by them, and continuing. The alternative to this is to not canonicalize rationals, canonicalize after, say, every five arithmetical operations, and so on. This can be an expensive operation, certainly much more expensive than, say, normalizing "1.20" to "1.2".
- Various operations, such as exponentiation and logarithm, almost never produce rational numbers given a rational argument, so one would have to specify a certain amount of precision as a second argument to these operations. By contrast, in, say, Decimal128, these operations do not require a second argument.
Fractions would be an interesting thing to pursue in TC39, and are in many ways complementary to Decimal. The use cases for rationals overlap somewhat with the use cases for decimals. Many languages in the Lisp tradition (e.g., Racket) include rationals as a basic data type, alongside IEEE 754 64-bit binary floating point numbers; Ruby and Python also include fractions in their standard library.
We see rationals as complementary to Decimal because of a mismatch when it comes to two of the core operations on Decimals:
- Rounding to a certain base-10 precision, with a rounding mode
- Conversion to a localized, human-readable string
These could be defined on rationals, but are a bit of an inherent mismatch since rationals are not base 10.
Rational may still make sense as a separate data type, alongside Decimal. Further discussion of rationals in #6.
With Decimal we do not envision new literal syntax—such as a 123.456_789m suffix (#7). In light of feedback from JS engine implementors across multiple TC39 plenary meetings, we are choosing not to add it. See Decimal literals under Future work for how a v1 stays compatible with adding literals later.
As noted above, Decimal uses the IEEE 754-2019 Decimal128 data model. We will offer a subset of the official Decimal128. There will be, in particular:
- a single NaN value--distinct from the built-in
NaNof JS. The difference between quiet and signaling NaNs will be collapsed into a single (quiet) Decimal NaN. - positive and negative infinity will be available, though, as with
NaN, they are distinct from JS's built-inInfinityand-Infinity. - negative zero (
-0) is a distinct value from positive zero, mirroring IEEE 754 and JavaScript'sNumber; the two compare as equal.
These special values are exposed as ordinary Decimal values rather than causing exceptions, so that computation can continue in exceptional conditions.
Decimal does not expose information about trailing zeroes. Thus, "1.20" is valid syntax, but there is no way to distinguish 1.20 from 1.2: equality compares mathematical values (so 1.20 and 1.2 are equal), and toString always renders a normalized String. Crucially, this is a guarantee about what Decimal exposes, not a requirement on how a value is stored. An implementation is free to keep values unnormalized internally and to normalize only on the way out, when rendering a String or comparing values. A motto that captures this is "normalize on the way out".
This hybrid stance is deliberate. Mike Cowlishaw, an author of IEEE 754's decimal arithmetic, explains in the Decimal Arithmetic FAQ why decimal arithmetic is typically performed unnormalized: addition and subtraction often need no alignment shifts, the coefficient and exponent computations stay independent, and results match what users expect from manual calculation. Because Decimal never exposes whether a value is normalized, an implementation can reap essentially all of those benefits (mainly performance) without forcing callers to reason about trailing zeroes. Not exposing trailing zeroes is, in turn, a deliberate omission from the capabilities defined by IEEE 754: trailing zeroes are display precision, not arithmetic, and tracking them is precisely the job of Amount. (Relatedly, the keep trailing zeroes proposal, now at Stage 3, ensures that Intl does not silently strip trailing zeroes when formatting digit strings.)
- Arithmetic
- Unary operations
- Absolute value
- Negation
- Binary operations
- Addition
- Multiplication
- Subtraction
- Division
- Remainder
- Unary operations
- Rounding: All five rounding modes of IEEE 754—floor, ceiling, truncate, round-ties-to-even, and round-ties-away-from-zero—will be supported.
- (This implies that a couple of the rounding modes in
Intl.NumberFormatandTemporalwon't be supported.)
- (This implies that a couple of the rounding modes in
- Comparisons
- equals and not-equals
- less-than, less-than-or-equal
- greater-than, greater-than-or-equal
- Mantissa, exponent, significand
The library of numerical functions here is kept deliberately minimal. It is based around targeting the main use case, in which fairly straightforward calculations are envisioned. For scientific or numerical computations, developers may experiment in JavaScript, developing such libraries, and we may decide to standardize these functions in a follow-on proposal; a minimal toolkit of mantissa, exponent, and significand will be available. We currently do not have good insight into the developer needs for this use case, except generically: square roots, exponentiation & logarithms, and trigonometric functions might be needed, but we are not sure if this is a complete list, and which are more important to have than others. In the meantime, one can use the various functions in JavaScript’s Math standard library.
- Bitwise operators are not supported, as they don’t logically make sense on the Decimal domain (#20)
- We currently do not foresee Decimal values interacting with other Number values. That is, TypeErrors will be thrown when trying to add, say, a Number to a Decimal, similar to the situation with BigInt and Number. (#10).
Decimal objects can be constructed from Numbers, Strings, and BigInts. Similarly, there will be conversion from Decimal objects to Numbers, String, and BigInts.
Decimal objects can be converted to plain, locale-independent Strings in a number of ways, similar to Numbers:
toString()is similar to the behavior on Number, e.g.,new Decimal("123.456").toString()is"123.456". (#12)toFixed()is similar to Number'stoFixed()toPrecision()is similar to Number'stoPrecision()toExponential()is similar to Number'stoExponential()
We intend to specify these methods in terms of abstract operations that render a mathematical value as a digit string (in fixed, precision-limited, or exponential form). These abstract operations are written so that the Amount proposal can reuse them, giving Decimal and Amount a single, consistent notion of how a decimal value becomes a string.
Decimal also supports locale-aware formatting: Decimal.prototype.toLocaleString produces a localized string for a Decimal value, analogous to Number.prototype.toLocaleString (#15). This is a basic need on the web, so Decimal offers it directly rather than requiring another type.
Decimal and Amount are designed to share the underlying formatting machinery rather than duplicate it; whether toLocaleString is specified by building an Amount internally or via common abstract operations is an implementation detail to be settled in the spec. For when to reach for which type, see Amount below.
Decimal has been under discussion in TC39 for a very long time, with proposals and feedback from many people including Sam Ruby, Mike Cowlishaw, Brendan Eich, Waldemar Horwat, Maciej Stachowiak, Dave Herman and Mark Miller. The earliest threads predate the modern proposal process:
- A new
decimaltype was long planned for ES4, see Proposed ECMAScript 4th Edition – Language Overview - In the following ES3.1/ES5 effort, discussions about a decimal type continued on es-discuss, e.g., [1] [2]
- Decimal was discussed at length in the development of ES6. It was eventually rolled into the broader typed objects/value types effort, which didn't make it into ES6, but is being incrementally developed now.
More recently, Decimal has been on the TC39 plenary agenda repeatedly:
- Decimal for stage 0 (November, 2017)
- BigDecimal for Stage 1 (February, 2020)
- Decimal update (March, 2020)
- Decimal stage 1 update (December, 2021)
- Decimal stage 1 update (March, 2023)
- Decimal open-ended discussion (July, 2023)
- Decimal stage 1 update and open discussion (September, 2023)
- Decimal stage 1 update and request for feedback (November, 2023)
- Decimal for stage 2 (April, 2024)
- Decimal for stage 2 (June, 2024)
The vision of decimal sketched here represents the champions current thinking and goals. In our view, decimal as sketched so far is a valuable addition to the language. That said, we envision improvements and strive to achieve these, too, in a version 2 of the proposal. What follows is not part of the proposal as of today, but we are working to make the first version compatible with these future additions.
In earlier discussions about decimal, we advocated for such overloading arithmetic operations (+, *, etc.) and comparisons (==, <, etc.), as well as ===. But based on strong implementer feedback, we have decided to work with the following proposal:
- In the first version of this proposal, we intend to make
+,*, and so on throw when either argument is a decimal value. Instead, one will have to use theadd,multiply, etc. methods. Likewise, comparison operators such as==,<,<=, etc. will also throw when either argument is a decimal. One should use theequalsandlessThanmethods instead. - The strict equality operator
===will work (won't throw an exception), but it will have its default object semantics; nothing special about decimal values will be involved.
However, the door is not permanently closed to overloading. It is just that the bar for adding it to JS is very high. We may be able to meet that bar if we get enough positive developer feedback and work with implementors to find a path forward.
In earlier discussions of this proposal, we had advocated for adding new decimal literals to the language: 1.289m (notice the little m suffix). Indeed, since decimals are numbers—essentially, basic data akin to the existing binary floating-point numbers—it is quite reasonable to aim for giving them their own "space" in the syntax.
However, as with operator overloading, we have received strong implementor feedback that this is very unlikely to happen.
Nonetheless, we are working on making sure that the v1 version of the proposal, sketched here, is compatible with a future in which decimal literals exist. As with operator overloading, discussions with JS engine implementors need to be kept open to find out what can be done to add this feature. (On the assumption that a v1 of decimals exists, one can add support for literals fairly straightforwardly using a Babel transform.)
In our discussions we have consistently emphasized the need for basic arithmetic. And in the v1 of the proposal, we in fact stop there. One can imagine Decimal having all the power of the Math standard library object, with mathematical functions such as:
- trigonometric functions (normal, inverse/arc, and hyperbolic combinations)
- natural exponentiation and logarithm
- any others?
These can be more straightforwardly added in a v2 of Decimal. Based on developer feedback we have already received, we sense that there is relatively little need for these functions. But it is not unreasonable to expect that such feedback will arrive once a v1 of Decimal is widely used.
See the discussion above, about data models, where rationals are discussed.
This depends on implementations. Like BigInt, implementors may decide whether or not to optimize it, and what scenarios to optimize for. We believe that, with either alternative, it is possible to create a high-performance Decimal implementation. Historically, faced with a similar decision of BigInt vs Int64, TC39 decided on BigInt; such a decision might not map perfectly because of differences in the use cases. Further discussion: #27
One option that’s raised is allowing for greater precision in more capable environments. However, Decimal is all about avoiding unintended rounding. If rounding behavior depended on the environment, the goal would be compromised in those environments. Instead, this proposal attempts to find a single set of semantics that can be applied globally.
Operator overloading is covered under Future work: the first version of Decimal deliberately does not overload +, <, and friends, though the door is not permanently closed. The relationship to BigInt, Amount, and the other allied proposals is covered in Relationship of Decimal to other TC39 proposals.
Many decimal implementations support a global option to set the maximum precision (e.g., Python, Ruby). In QuickJS, there is a “dynamically scoped” version of this: the setPrec method changes the maximum precision while a particular function is running, re-setting it after it returns. Default rounding modes could be set similarly.
Although the dynamic scoping version is a bit more contained, both versions are anti-modular: Code does not exist with independent behavior, but rather behavior that is dependent on the surrounding code that calls it. A reliable library would have to always set the precision around it.
There is further complexity when it comes to JavaScript’s multiple globals/Realms: a Decimal primitive value does not relate to anything global, so it would be inviable to store the state there. It would have to be across all the Decimals in the system. But then, this forms a cross-realm communication channel.
Therefore, this proposal does not contain any options to set the precision from the environment.
Mike Cowlishaw’s excellent Decimal FAQ explains many of the core design principles for decimal data types, which this proposal attempts to follow.
One of those principles—that decimal arithmetic is most naturally performed on unnormalized values—shapes Decimal's "normalize on the way out" stance, discussed under Data model.
This proposal can be seen as a follow-on to BigInt, which brought arbitrary-sized integers to JavaScript and was standardized in ES2020. However, unlike BigInt, Decimal (i) does not introduce a new primitive data type, (ii) does not propose operator overloading (which BigInt does support), and (iii) does not offer new syntax (numeric literals), which BigInt does add (e.g., 2345n).
Decimal is designed to work hand-in-hand with the TC39 Amount proposal (Stage 2). The two proposals split the problem of working with decimal quantities this way:
Decimal computes the value
and
Amount carries a value's precision and unit.
Amount wraps a numeric value with its measurement context (significant or fraction digits), as well as, optionally, a unit (including a currency). Amount deliberately does not perform arithmetic (at most, it does rounding after a certain number of digits). Conversely, Decimal deliberately does not track display precision and has no concept of units. Decimal values can be rendered in a locale-sensitive way with toLocaleString. Amount is what adds the precision and unit/currency data, and it can take a Decimal to do so.
| Concern | Owner |
|---|---|
Exact +, −, ×, ÷, remainder, and rounding |
Decimal |
| Comparisons | Decimal |
| Extracting mantissa/exponent/significand | Decimal |
| Handling significant/fraction digits (including trailing zeroes) | Amount |
| Units and currency | Amount |
Plain string rendering (toString, toFixed, toPrecision, toExponential) |
Decimal (via shared rendering AOs that Amount can reuse) |
Locale-aware formatting (toLocaleString) |
Decimal for a bare number; Amount when units, currency, or display precision are involved |
| Plural selection | Amount (Decimal may lose relevant data) |
| Unit conversion | Amount |
Decimal and Amount sit within a small family of TC39 proposals that together cover working with decimal quantities. Beyond Amount:
- Keep trailing zeroes (Stage 3): ensures that
Intldoes not silently strip trailing zeroes when formatting digit strings (e.g., unintentionally normalizing"1.20"to"1.2"). - Smart Units: a follow-on to Amount for locale- and usage-aware unit preferences and conversion.
- Experimental implementation in QuickJS, from release 2020-01-05 (use the
--bignumflag) - decimal128.js is an npm package that implements Decimal128 in JavaScript (more precisely, the variant of Decimal128 that we envision for this proposal)
- We are looking for volunteers for writing a polyfill along the lines of JSBI for both alternatives, see #17
Your help would be really appreciated in this proposal! There are lots of ways to get involved:
- Share your thoughts on the issue tracker
- Document your use cases, and write sample code with decimal, sharing it in an issue
- Research how decimals are used in the JS ecosystem today, and document what works and what doesn’t, in an issue
- Help us write and improve documentation, tests, and prototype implementations
See a full list of to-do tasks at #45.