The TC39 Decimal proposal gives JavaScript an exact base-10 number type
Built-in exact decimal numbers for JavaScript
At a glance
- What is it?
- proposal-decimal is a TC39 stage 1 proposal to add exact base-10 decimal arithmetic to JavaScript, so that 0.1 plus 0.2 is 0.3 and financial calculations stop inheriting binary floating-point error. It is a specification repository, not a library, and it is explicitly separated from a companion proposal for currency-aware display and localisation.
- Who is it for?
- Adopt the Decimal design as your reference if you are building a money or accounting path in JavaScript today, because it documents the ergonomics a native decimal type would need and it is the only artefact of its kind, but do not plan a migration to it yet, because it is at stage 1 of the TC39 process and the API it describes does not exist in any shipping engine. Do not adopt it as a library either, since the repository is a specification and contains no runtime code.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 116 days ago.
- What is it written in?
- Mainly HTML, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What problem the proposal is solving, in the committee's own terms
The TC39 Decimal proposal aims to add exact decimal arithmetic to JavaScript, eliminating the rounding errors that occur with binary floating-point numbers.
The README offers two reasons 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: the goal is a numeric type that behaves the way we were taught arithmetic works, so that 0.1 + 0.2 is 0.3. The proposal describes Decimal as a do-what-I-mean approach to numbers, where the value and the everyday mental model line up with no hidden binary representation oddities.
The problem statement is concrete about the current situation. Accurate storage and processing of base-10 decimal numbers is a frequent need in JavaScript, and today developers sometimes represent these using libraries for the purpose, or sometimes use Strings. The README says sadly that JavaScript Numbers are also sometimes used, leading to real, end-user-visible rounding errors.
The mechanism of the error is spelled out. Numbers are 64-bit binary floating-point numbers, and the conversion from most decimal values to binary floats rarely is an exact match. The decimal number 0.5 can be exactly represented in binary, but not 0.1, and the 64-bit floating point number corresponding to 0.1 is actually 0.1000000000000000055511151231257827021181583404541015625. The same goes for 0.2, 0.3 and the rest, and statistically most human-authored decimal numbers cannot be exactly represented as a binary floating point number.
The stage is stated clearly. This is stage 1 of the TC39 process, with a draft specification available, and the champions are Jesse Alama of Igalia, Jirka Maršík of Oracle and Andrew Paprocki of Bloomberg, with Jesse Alama and Waldemar Horwat as authors.
Why the cents workaround fails for real money
The section on exact decimal arithmetic for financial calculations is the most concretely useful part of the README, because it explains why the standard workaround of storing money as integer cents is not good enough.
The first issue is a persistent mismatch between the way humans think about money and the way it is manipulated in the program, which causes mental overhead for the programmer who is aware of it. The README is careful about the second audience: some programmers may not even be aware of this mismatch, and that opens the door to rounding errors whose source is unknown, and if calculations start to get more involved the chance of error increases.
The second issue is internationalisation. Different currencies use different numbers of decimal positions, which is easy to get confused, and the hack of working with quantities implicitly multiplied by 100 may not work when working with multiple currencies. The example given is that it is not correct to assume that all currencies have two decimal places, or that the only exception is JPY, the Japanese yen, and making such assumptions makes it hard to internationalise code to new countries.
The third issue is that sometimes fractional cents need to be represented too, with the examples given being precise prices that occur in stock trading or currency conversion.
That third point is the one that quietly breaks the cents convention. Once a value can be smaller than a cent, the implicit multiplication factor stops being an integer that you can reason about, and the whole scheme needs a real decimal representation underneath. A type that carries scale and precision explicitly, rather than a scaled integer, is what the proposal is reaching for.
The proposed API: constructing from strings and chaining operations
The sample code in the README shows what a Decimal API would look like in practice, and the shape is instructive. Decimal is constructed from a string rather than from a number literal, which is what preserves the exact decimal value.
The bill example sums line items and applies a tax rate:
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)));
}Every value that comes from outside the program is passed as a string, and every operation returns a new value rather than mutating one, so the API reads like an immutable arithmetic type. That is the familiar shape from decimal libraries in other languages, and the proposal is essentially arguing that JavaScript should have one built in.
The tax rate in the example is itself a Decimal, and the multiplication by tax plus one is how the rate is applied. The rate string in the example is 0.0735, a figure with four decimal places, which is another hint about the scale problem: a rate of 7.35 percent is not representable as a whole number of cents either.
One detail that stands out for anyone porting code: the constructor takes a string in the sample, and the arithmetic methods are named add, times and multiply. Whether the proposal also accepts numbers in the constructor, and what happens when it does, is a question the draft specification has to answer rather than the README.
The division of labour with the Amount proposal
The currency conversion example is the part of the README that shows the proposal's limits, and the limits are deliberate.
// 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);The exact division is Decimal's job, and the README then says that the result is handed to Amount to round it for display, attach the currency and localise it, with the display side being Amount's job covering precision, currency and locale.
The point is that a decimal type alone does not solve money handling. A Decimal knows the value and can divide it exactly, but it does not know that EUR is shown with two fraction digits, that the German locale uses a comma as the decimal separator, or that the result should be rounded rather than truncated at the point of display. Those are presentation and formatting decisions, and the proposal deliberately keeps them out.
The two are bridged with toString() today, and the README notes that a natural point of coordination between the proposals is for Amount to accept a Decimal value directly, so the handoff becomes passing the Decimal into the Amount constructor with the unit and fraction digits. That is a small technical detail and a large architectural statement, because it fixes where the line between computation and presentation falls, and it means the rounding rule is specified once, in the proposal that owns currency rather than in every application that multiplies by a tax rate.
The separation is why the currency example in the README is the one worth reading carefully, and it is also why anyone evaluating this should read the companion Amount proposal alongside it rather than treating Decimal as a drop-in money type.
Why JavaScript needs this now, and the current workarounds
The README has a section asking why use JavaScript for this case at all, and the answer is a list of trends rather than a single argument.
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 such as 1.25, and never does calculations or conversions at all. The proposal then lists three trends pushing JS deeper into decimal territory.
More complicated frontend architectures, where rounding, localisation or other presentational aspects may be performed on the frontend for better interactive performance. Serverless, where many systems use JavaScript in order to better leverage the knowledge of frontend engineers. And server-side programming in JavaScript, with systems like Node.js and Deno having grown in popularity for more traditional server-side work.
The list of current workarounds follows the same structure. An external library could be used instead, which introduces the issues of choosing the library and coordinating on its use. Calculations could be in terms of cents, which is fallible as explained above. And 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 think they are being careful.
That last entry is the argument in miniature. The problem is not that a careful engineer cannot avoid the issue with Number, it is that the issue is invisible until it is not, and the cost of a native type is that the safe choice is also the easy one.
How the specification itself is built and read
This repository is the specification, and it is worth looking at how it is produced, because that determines how you should read it and how quickly it will change.
The specification source is spec.emu, with a separate intl.emu for the internationalisation related text, and the published draft is index.html. The Makefile is small and shows the build clearly: the default target builds index.html from spec.emu and intl.emu using ecmarkup with the spec linter and the strict flag, loading the ecma262 and ecma402 bibliographies, and the clean target simply removes the generated file.
index.html: spec.emu intl.emu
npx ecmarkup --lint-spec --strict --load-biblio @tc39/ecma262-biblio --load-biblio @tc39/ecma402-biblio spec.emu $@The package.json mirrors that with a build-loose script that runs ecmarkup with the verbose flag and the lint-spec flag, loading the same two bibliographies, and a build script that adds the strict flag. The docs have their own pipeline, with a docs:test script that runs a script named testExamples.mjs, meaning the code samples in the documentation are executed, and a docs:build script that runs the test before building the documentation with buildDocs.js.
There is also a decimal128-reference.md file at the repository root, which is where the precision and range of the type are set out, and the keywords in the manifest list Decimal, BigDecimal and Decimal128, which tells you the shape of the numeric space the proposal is negotiating.
For a reader, the practical consequence is that this repository is a working document. A stage 1 proposal is exploratory, the README asks for input through a survey, and the design space is explicitly open. The last push was on 2026-06-30, and the repository is not archived, so the discussion is live.
Where this proposal is the wrong tool
Three limits, and the first one is a matter of timing rather than design.
The first is that there is nothing to install. This is a specification repository with no runtime implementation, so if your problem today is that 0.1 + 0.2 is not 0.3 in your accounting code, this proposal does not help you, because no engine ships a Decimal type. What it gives you is a design you can read, and a set of semantics you can hold a third-party decimal library to.
The second is that the proposal does not, on its own, solve the money problem, as the currency example makes clear. If you need correct currency handling in JavaScript today, you need a decimal library and a formatting library, and the proposal is essentially a description of the interface the first of those should have. The value of reading it is in the semantics it settles, particularly around string construction, immutability and the division behaviour, since those are the parts libraries disagree on most.
The third is that it is a language change, and language changes are slow. Stage 1 of the TC39 process is the exploratory stage, and the README is candid that the design space is open and that input is being sought. A stage 1 proposal can change substantially, and the interesting question for an engineer is not whether the proposal is good but when a shipping engine might implement something like it, which is a question the repository does not answer.
The alternative approach, and it is the one available today, is to use an existing decimal library and be explicit about the boundary in your own code. The difference in approach is that the library gives you a fixed API and a fixed set of rounding rules that you must understand yourself, while the proposal aims to give the language a single native type with specified semantics so that every library and every future engine agrees on what a decimal value is. For a team that is standardising on a house decimal library, the proposal is a useful check on whether your own library's semantics match where the language is heading.
Editorial conclusion
Adopt the Decimal design as your reference if you are building a money or accounting path in JavaScript today, because it documents the ergonomics a native decimal type would need and it is the only artefact of its kind, but do not plan a migration to it yet, because it is at stage 1 of the TC39 process and the API it describes does not exist in any shipping engine. Do not adopt it as a library either, since the repository is a specification and contains no runtime code. Verify first that the Decimal API your design assumes matches the current draft, since stage 1 proposals change, that the companion Amount proposal you would pair with for display is where the rounding and locale rules live, and that the decimal128-reference.md file in the repository reflects the precision and range you are designing against.
Frequently asked questions
What is the TC39 Decimal proposal and what does it add to JavaScript?
It aims to add exact decimal arithmetic to JavaScript, eliminating the rounding errors that occur with binary floating-point Numbers. The goal is a numeric type where 0.1 + 0.2 is 0.3, described as a do-what-I-mean approach to numbers, and it is currently at stage 1 of the TC39 process with a draft specification available.
Why is storing money as integer cents not a good solution?
The README lists three reasons: a persistent mismatch between how humans think about money and how it is manipulated in the program, some programmers not being aware of the mismatch at all, and different currencies using different numbers of decimal positions, so the assumption of two decimal places with JPY as the only exception does not hold and makes internationalisation hard. Fractional cents also have to be representable, as in stock trading or currency conversion.
How would a Decimal be constructed and used in the proposal's examples?
Decimal values are constructed from strings, for example new Decimal("1.25"), and the operations are immutable and chained, with add, times and multiply shown in the bill example and divide in the currency conversion example. Exact computation stays with Decimal, and rounding, currency and locale are handed to the separate Amount proposal.
How does the Decimal proposal relate to the Amount proposal?
The README says the two types are bridged today with toString(), and that a natural point of coordination is for Amount to accept a Decimal value directly. Decimal handles the exact arithmetic, while Amount handles precision, currency and locale for display.
How is the proposal-decimal specification built and where is the numeric range defined?
The specification source is spec.emu with intl.emu alongside it, built into index.html with ecmarkup using the spec linter, the strict flag and the ecma262 and ecma402 bibliographies, via the Makefile or the package.json build scripts. The precision and range of the type are set out in decimal128-reference.md at the repository root.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tc39-proposal-decimal)Was this useful?