NestJS Module 5 – Pipes, validation and DTOs
Writing custom pipes in NestJS
Write NestJS pipes that convert and check one argument with PipeTransform and ArgumentMetadata, and decide what belongs in a pipe, a guard or an interceptor.
What you will learn
- Write synchronous and asynchronous PipeTransform classes that read ArgumentMetadata
- Choose what belongs in a pipe, a guard, an interceptor, middleware or a service
- Load an entity by id in a pipe that injects a repository, and turn a miss into a 404
- Fix the compiler errors that Nest 12's pipe types raise in hand-written pipes, and keep internal details out of pipe errors
Before you start
What you’ll learn · Before you start
What you will learn
- Write synchronous and asynchronous PipeTransform classes that read ArgumentMetadata
- Choose what belongs in a pipe, a guard, an interceptor, middleware or a service
- Load an entity by id in a pipe that injects a repository, and turn a miss into a 404
- Fix the compiler errors that Nest 12's pipe types raise in hand-written pipes, and keep internal details out of pipe errors
Before you start
On this page
A wallet top-up sends its amount as the text "1234.50", and the handler needs 123450 paise. Converting it inside
every handler that takes money repeats the same code and invites the classic slip: in JavaScript 4.35 * 100 is
434.99999999999994, so Math.floor() loses a paisa.
This lesson writes the conversion once, as a custom pipe that runs before the handler, then decides where other work belongs: in a pipe, a A class whose canActivate() method decides, before any pipe runs, whether a request may reach its handler; returning false or throwing refuses the request., an interceptor, middleware or a service. You will write a pipe class, read what Nest tells it about the argument, and turn bad input into a clear 400.
A pipe is a class with one method, transform(value, metadata)
A custom pipe implements PipeTransform: The interface every Nest pipe implements: one method, transform(value, metadata), whose return value replaces the handler's argument and whose thrown exception becomes the response. It may return a promise.: one method, transform(value, metadata),
whose return value replaces the argument. ParsePaisePipe accepts rupees as text with at most two decimals and
returns whole paise, computed from the digits, so no binary fraction is ever multiplied:
import { type ArgumentMetadata, BadRequestException, Injectable, type PipeTransform } from '@nestjs/common';
// Rupees as text with at most two decimals, "1234.50" or "99": no sign, no digit grouping, no exponent.
const RUPEES = /^(\d{1,7})(?:\.(\d{1,2}))?$/;
// Turns an amount in rupees, sent as text, into a whole number of paise, or answers 400.
@Injectable()
export class ParsePaisePipe implements PipeTransform<unknown, number> {
transform(value: unknown, metadata: ArgumentMetadata): number {
console.log(` transform(${JSON.stringify(value)}, { type: '${metadata.type}', data: '${metadata.data}', metatype: ${metadata.metatype?.name} })`);
const field = metadata.data ?? 'amount';
if (value === undefined) throw new BadRequestException(`${field} is missing`);
if (typeof value !== 'string') throw new BadRequestException(`${field} must be text such as "1234.50", not a ${typeof value}`);
const match = RUPEES.exec(value);
if (!match) throw new BadRequestException(`${field} must be rupees with at most two decimals, such as "1234.50"`);
const [, rupees, fraction = ''] = match;
return Number(rupees) * 100 + Number(fraction.padEnd(2, '0'));
}
} Step through one amount, then run the endpoint with seven bodies:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- The body. A client posts {"amount":"1234.50"}. The amount is text on purpose: a JSON number would arrive as a binary fraction, and 4.35 * 100 is 434.99999999999994 in JavaScript.
- The argument. @Body('amount', ParsePaisePipe) takes one property of the parsed body, the string '1234.50', and hands it to the pipe before the handler can run.
- The call. Nest calls transform('1234.50', metadata), and metadata says type 'body', data 'amount' and metatype Number: where the value came from, its name and the type the handler wants.
- The check. The pipe matches the text against its pattern, at most seven digits of rupees and at most two decimals, and reads the rupees and the paise as two whole numbers.
- The result. 1234 rupees times 100, plus 50 paise, is 123450. The return value replaces the argument, so the handler receives a whole number of paise and never the text.
- The handler. topUp(123450) runs, and the client gets 201 with {"creditedPaise":123450}.
- Three decimals. "12.345" has three decimals, so it does not match the pattern. The pipe throws BadRequestException with a message that names the field and the expected form.
- The 400. The handler never runs. The client gets 400: amount must be rupees with at most two decimals, such as "1234.50".
import { session } from '../session.mjs';
// Builds a wallet top-up endpoint whose amount goes through ParsePaisePipe, then sends it seven bodies.
session(`
npx tsc -p tsconfig.json
node dist/main.js
`); Output
$ npx tsc -p tsconfig.json
$ node dist/main.js
POST /wallet/top-ups {"amount":"1234.50"}
transform("1234.50", { type: 'body', data: 'amount', metatype: Number })
-> 201 {"creditedPaise":123450}
POST /wallet/top-ups {"amount":"99"}
transform("99", { type: 'body', data: 'amount', metatype: Number })
-> 201 {"creditedPaise":9900}
POST /wallet/top-ups {"amount":"0.5"}
transform("0.5", { type: 'body', data: 'amount', metatype: Number })
-> 201 {"creditedPaise":50}
POST /wallet/top-ups {"amount":"12.345"}
transform("12.345", { type: 'body', data: 'amount', metatype: Number })
-> 400 {"message":"amount must be rupees with at most two decimals, such as \"1234.50\"","error":"Bad Request","statusCode":400}
POST /wallet/top-ups {"amount":"1,234.50"}
transform("1,234.50", { type: 'body', data: 'amount', metatype: Number })
-> 400 {"message":"amount must be rupees with at most two decimals, such as \"1234.50\"","error":"Bad Request","statusCode":400}
POST /wallet/top-ups {"amount":1234.5}
transform(1234.5, { type: 'body', data: 'amount', metatype: Number })
-> 400 {"message":"amount must be text such as \"1234.50\", not a number","error":"Bad Request","statusCode":400}
POST /wallet/top-ups {}
transform(undefined, { type: 'body', data: 'amount', metatype: Number })
-> 400 {"message":"amount is missing","error":"Bad Request","statusCode":400}
4.35 * 100 in JavaScript is 434.99999999999994, and Math.floor() of it is 434
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs
The second argument is ArgumentMetadata: What Nest passes to a pipe about the argument: type (body, query, param or custom), data (the name given to the decorator, if any), metatype (the declared type of the parameter) and, in Nest 12, schema., and the run printed it for every
request: type: 'body' because the value came from @Body(), data: 'amount' because the decorator named one
property, and metatype: Number, the handler’s declared type, which is what the pipe must produce. The pipe used
data to name the field in its messages. "12.345" and "1,234.50" got 400 with an actionable message, the number
1234.5 got 400 because a JSON number cannot promise two exact decimals, and a missing amount got amount is missing. An exception thrown in a pipe is a response: the handler never ran for those four.
transform() may also return a promise, and Nest waits for it before the next pipe or the handler runs, which is
how a pipe can look something up in a database. Bind a pipe by class, @Body('amount', ParsePaisePipe), and Nest
builds it and can inject its dependencies; bind an instance, as with new ParseEnumPipe(OrderStatus), when the pipe
takes options in its constructor.
Checkpoint
Check: The PipeTransform contract
A pipe changes one argument, and the other enhancers do the rest
A pipe sees one argument at a time, after Nest has extracted it, and nothing about the user, the route’s metadata or the handler’s result. That decides what belongs in it. Nest runs a request through middleware, then guards, then interceptors, then pipes and the handler, and the response back through the interceptors:
| Work | Where it belongs | Why there |
|---|---|---|
Turn "1234.50" into 123450, check a UUID |
a pipe | it receives the one value, already extracted |
| Decide whether this caller may use the route | a guard | it runs before pipes and knows the handler through its execution context |
| Time the call, map the result, cache a response | an interceptor | it wraps the handler and sees its result |
| Read the raw request before routing, for a request id or a body limit | middleware | it runs first, before Nest knows the handler |
| Check stock, ownership, a daily limit | a service | the rule needs data, and one place to keep it |
Choose for your own case in the table below; it marks what each answer rules out:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Place | Sees one parsed argument | Can refuse the request | Sees the handler's result | Knows which handler runs |
|---|---|---|---|---|
| A pipe Receives: one argument, extracted | Good gets the value after extraction, with its ArgumentMetadata | Good a thrown exception becomes the answer; the handler never runs | Poor runs before the handler only | Poor knows the argument's type and name, not the route or the user |
| A guard Receives: the execution context | Poor runs before pipes, on the raw request | Good returns false for 403, or throws its own exception | Poor runs before the handler only | Good reads the handler and its metadata from the execution context |
| An interceptor Receives: the context and the result | Poor its first part runs before pipes, on the raw request | Mixed can throw, or answer without calling the handler | Good wraps the handler and maps, times or caches its result | Good reads the handler from the execution context |
| Middleware Receives: the raw request and response | Poor runs before routing has chosen a handler | Good can end the response itself | Mixed only by listening on the response | Poor does not know which handler will run |
| A service Receives: what the handler passes | Mixed gets what the handler passes, after the pipes | Mixed throws a domain error that a filter or the handler turns into a status | Good computes the result | Mixed knows the use case, not the HTTP route |
How the answers decide
- “What does the code act on?” One argument's form: rules out A guard, An interceptor and Middleware. Only a pipe receives one extracted argument with its metadata; guards, interceptors and middleware see the raw request before pipes run.
- “What does the code act on?” Who calls, and what they may do: rules out A pipe and Middleware and adds a caveat to An interceptor. A guard knows the handler and runs before any pipe or interceptor; a pipe cannot see the user, and middleware does not know the route.
- “What does the code act on?” The handler's result or timing: rules out A pipe and A guard. Only an interceptor wraps the handler and receives what it returns.
- “What does the code act on?” The raw request, before routing: rules out A pipe, A guard and An interceptor. Middleware runs first, before Nest has chosen a handler, which suits request ids, body limits and CORS.
- “Does the decision need data from a database or another service?” Yes and “What does the code act on?” One argument's form: adds a caveat to A pipe. A pipe may load a record by id, but a rule about stock, ownership or limits belongs in the service that owns the data.
- “Does the decision need data from a database or another service?” Yes and “What does the code act on?” Who calls, and what they may do: adds a caveat to A guard. A guard that needs a record must load it itself, because guards run before the pipe that would load it; ownership checks often fit the service better.
Key idea
A pipe converts and checks one argument and refuses bad input with a 400 before the handler runs; access belongs to guards, work around the handler to interceptors, and rules that need data to services.
Three more matters have trade-offs of their own. A pipe bound by class can inject a provider, such as a repository that turns a kitchen id into the kitchen itself. Nest 12 changed the types of the built-in pipes, which hand-written pipes that call them must follow. And whatever a pipe throws reaches the client as its answer, so its messages need the care of any response.
Checkpoint
Check: Pipe, guard or interceptor
Pipes that load data: injection, async lookups and 404s, with a wiring error reproduced Pro: opens with a Pro, Premium or Ultimate pass
A pipe can turn an id into the record it names, so a handler receives a kitchen instead of a number. The run in this part builds such a pipe, counts how often Nest creates it, and reproduces the start-up error of a module that cannot give it its repository.
In this part:
- A pipe that injects a repository turns a kitchen id into a kitchen
- A global ValidationPipe checks the raw id against the type the pipe will produce, a trap reproduced
- Bound by class, a pipe is built in the module of its controller, a wiring error reproduced
- Guards run before the pipe, so access checks on the loaded record go in the service
2 runnable examples (JavaScript, TypeScript) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Upgrading and hardening hand-written pipes, with the Nest 12 type errors and an error leak reproduced Pro: opens with a Pro, Premium or Ultimate pass
Hand-written pipes meet two problems that the examples so far avoided: Nest 12 changed the types of the built-in pipes they call, and a pipe that repeats an internal error tells clients too much. Both are reproduced in this part, with their fixes.
In this part:
- Code that calls a built-in pipe can stop compiling on Nest 12
- An error from a pipe is an answer to a client: keep internals out of it, a leak reproduced
- A checklist for every custom pipe
3 runnable examples (JavaScript, TypeScript) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: build a money pipe in two steps
The lab builds the two halves of a production ParsePaisePipe: the function that reads an amount, then the chain
that runs a parameter’s pipes the way Nest does. The sample tests of both steps run in your browser.
Rupee amounts with Indian digit grouping
The first step extends the lesson’s pipe to the amounts people type in India: the rupee sign, and digits grouped
in lakhs and thousands as in ₹1,23,456, besides the plain 1234.5.
Code lab · 2 steps
Build a money pipe
Parse rupee amounts into whole paise, then run a parameter's pipes in order, as Nest does, with a default before the parse.
Exercise · Medium · JavaScript
Parse rupee amounts into paise
Write parseRupeesToPaise(input), the function a production ParsePaisePipe would call. It returns the amount as a whole number of paise, or null for any input it refuses; the pipe turns null into a 400.
Accept text only, written like this:
- an optional rupee sign
₹at the very start, with no space after it; - the rupees: digits without separators (
123456), or grouped with commas in thousands (123,456and1,234,567), or grouped the Indian way, the last three digits together and then pairs (1,23,456and12,34,567); at most nine digits in all; - optionally a point and one or two digits of paise (
1234.5is 123450 paise,0.05is 5).
Refuse everything else, and in particular what a reader could take two ways: 12,34 (is the comma a decimal point?), three digits after the point (12.345), groups that fit neither system (1,2345, 1,234,56), signs, exponents, spaces, an empty string and any value that is not a string. Compute the paise from the digits, never by multiplying a decimal number by 100.
Starter code · rupees.mjs
/**
* Parses a rupee amount typed by a person into whole paise: "1234.5", "₹1,234.50" and "1,23,456" are accepted;
* returns null for anything it refuses, so a pipe can answer 400.
*/
export function parseRupeesToPaise(input) {
// Handles plain amounts only, as the lesson's pipe does: add the rupee sign and both kinds of digit grouping.
const match = /^(\d{1,9})(?:\.(\d{1,2}))?$/.exec(input);
if (!match) return null;
return Number(match[1]) * 100 + Number((match[2] ?? '').padEnd(2, '0'));
} The sample tests · rupees.test.mjs
import { assert, test } from 'mysmartcopilot:test';
import { parseRupeesToPaise } from './rupees.mjs';
test('plain rupees, with and without paise', () => {
assert.equal(parseRupeesToPaise('1234.5'), 123450);
assert.equal(parseRupeesToPaise('1234.50'), 123450);
assert.equal(parseRupeesToPaise('99'), 9900);
assert.equal(parseRupeesToPaise('0.05'), 5);
});
test('the rupee sign at the start', () => {
assert.equal(parseRupeesToPaise('₹1,234.50'), 123450);
assert.equal(parseRupeesToPaise('₹99'), 9900);
});
test('Indian digit grouping', () => {
assert.equal(parseRupeesToPaise('1,23,456'), 12345600);
assert.equal(parseRupeesToPaise('12,34,567.89'), 123456789);
});
test('grouping in thousands', () => {
assert.equal(parseRupeesToPaise('123,456'), 12345600);
assert.equal(parseRupeesToPaise('1,234,567'), 123456700);
});
test('a comma before two digits could be a decimal point, so it is refused', () => {
assert.equal(parseRupeesToPaise('12,34'), null);
});
test('three digits after the point are refused', () => {
assert.equal(parseRupeesToPaise('12.345'), null);
});
test('groups that fit neither system are refused', () => {
for (const text of ['1,2345', '1,234,56', ',123', '1,,234', '12,34,56']) assert.equal(parseRupeesToPaise(text), null, text);
});
test('signs, exponents, spaces and empty text are refused', () => {
for (const text of ['-5', '+5', '1e3', '', '₹', ' 12', '12 ', '₹ 12', '1234.', '.5']) assert.equal(parseRupeesToPaise(text), null, JSON.stringify(text));
});
test('values that are not text are refused', () => {
for (const value of [1234.5, undefined, null, ['12']]) assert.equal(parseRupeesToPaise(value), null, String(value));
});
test('at most nine digits of rupees', () => {
assert.equal(parseRupeesToPaise('99,99,99,999'), 99999999900);
assert.equal(parseRupeesToPaise('1,00,00,00,000'), null);
}); A hint
Take off the rupee sign, then split the text with one regular expression into the rupee part and an optional paise part. Check the rupee part against three patterns, plain digits, groups of three and the Indian pattern (one or two digits, any number of pairs, then three digits), and accept it if one of them matches. Remove the commas, and add the paise padded to two digits.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Exercise · Easy · JavaScript
Run a parameter's pipes in order
@Body('amount', new DefaultValuePipe('0'), ParsePaisePipe) gives one argument two pipes, and Nest runs them the way the lesson showed. Write runPipes(value, metadata, pipes), which does the same for a list of pipe objects, each with a method transform(value, metadata):
- call the pipes in the order of the list; each receives what the previous one returned, and the first receives
value; - give every pipe the same
metadataobject; - a
transform()may return a promise: wait for it before the next pipe runs; - when a pipe throws, or its promise is rejected, stop: no later pipe runs, and the promise that
runPipesreturns is rejected with that same error; - return a promise of the last pipe's result, or of
valueitself when the list is empty.
Starter code · run_pipes.mjs
/**
* Runs a parameter's pipes as Nest does: in order, each on the result of the one before, awaiting each, with the same
* metadata, stopping at the first error. Returns a promise of the final value.
*/
export function runPipes(value, metadata, pipes) {
let current = value;
for (const pipe of pipes) {
// Each pipe should receive the previous result, and a promise should be awaited before the next pipe runs.
current = pipe.transform(value, metadata);
}
return Promise.resolve(current);
} The sample tests · run_pipes.test.mjs
import { assert, test } from 'mysmartcopilot:test';
import { runPipes } from './run_pipes.mjs';
const meta = { type: 'body', data: 'amount' };
/** A pipe that appends a letter, to see the order. */
const append = (letter) => ({ transform: (v) => `${v}${letter}` });
/** The lesson's two pipes for an amount: a default for a missing value, then rupees to whole paise. */
const defaultZero = { transform: (v) => (v === undefined || v === null ? '0' : v) };
const parsePaise = {
transform(v, m) {
const match = /^(\d{1,7})(?:\.(\d{1,2}))?$/.exec(typeof v === 'string' ? v : '');
if (!match) throw new Error(`${m.data} must be rupees with at most two decimals`);
return Number(match[1]) * 100 + Number((match[2] ?? '').padEnd(2, '0'));
},
};
test('no pipes: the value as it was', async () => {
assert.equal(await runPipes('1234.50', meta, []), '1234.50');
});
test('each pipe receives the result of the one before, in order', async () => {
assert.equal(await runPipes('', meta, [append('a'), append('b'), append('c')]), 'abc');
});
test('a promise is awaited before the next pipe runs', async () => {
const slowDouble = { transform: (v) => new Promise((done) => setTimeout(() => done(v * 2), 5)) };
const addOne = { transform: (v) => v + 1 };
assert.equal(await runPipes(10, meta, [slowDouble, addOne]), 21);
});
test('every pipe gets the same metadata object', async () => {
const seen = [];
const spy = { transform: (v, m) => (seen.push(m), v) };
await runPipes('x', meta, [spy, spy]);
assert.equal(seen.length, 2);
assert.ok(seen[0] === meta && seen[1] === meta, 'the metadata passed on unchanged');
});
test('the money chain of the lesson: a default, then paise', async () => {
assert.equal(await runPipes(undefined, meta, [defaultZero, parsePaise]), 0);
assert.equal(await runPipes('12.5', meta, [defaultZero, parsePaise]), 1250);
});
test('a pipe that throws stops the chain and rejects with its error', async () => {
let later = false;
const after = { transform: (v) => ((later = true), v) };
await assert.rejects(runPipes('12.345', meta, [parsePaise, after]), 'amount must be rupees with at most two decimals', 'the error of parsePaise');
assert.equal(later, false, 'no pipe runs after the one that threw');
});
test('a rejected promise stops the chain as well', async () => {
let later = false;
const failing = { transform: () => Promise.reject(new Error('kitchen 99 not found')) };
const after = { transform: (v) => ((later = true), v) };
await assert.rejects(runPipes(99, meta, [failing, after]), 'kitchen 99 not found', 'the rejection of the lookup');
assert.equal(later, false, 'no pipe runs after the rejected one');
}); A hint
Make runPipes an async function and keep the current value in a variable. In a for…of loop over the pipes, replace it with await pipe.transform(current, metadata). An error thrown inside an async function already rejects its promise, so the loop needs no try at all.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the questions on the pipe contract and on where work belongs:
Practice that adapts to you
Practise this lesson
6 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
What is a custom pipe in NestJS, and what can its transform() method do with a handler's argument?
A custom pipe is a class that implements PipeTransform: one method, transform(value, metadata), which Nest calls with one argument of the handler before the handler runs. What it returns replaces the argument, so a pipe can convert a value, such as the text "1234.50" into 123450 paise in the lesson's run, or check it and return it unchanged. If it throws an HTTP exception, that becomes the response and the handler never runs, as the 400 for "12.345" showed. The metadata says where the value came from (body, query, param or custom), the property name given to the decorator and the declared type of the parameter. transform() may return a promise, which Nest awaits, and a pipe bound by class is built by Nest, so it can inject providers.
-
Mid to senior
A teammate proposes OrderByIdPipe, which loads an order by id and also refuses orders that do not belong to the current user. Assess the design: what belongs in the pipe, what does not, and where does the rest go?
-
Fresher to mid
After you move OrdersController into a new OrdersModule, the app fails to start with "Nest can't resolve dependencies of the KitchenByIdPipe (?)". The pipe worked in KitchensModule. Explain the error and fix it properly.
-
Mid to senior
A security test found that your NestJS API returned a database host name and a private address in 400 answers during a database outage. The answers come from a custom pipe that loads records. Explain the defect, fix it, and show that the fix works.
Key takeaways
- A custom pipe implements
transform(value, metadata): its return value replaces the argument, a thrown exception becomes the response, and the method may return a promise that Nest awaits. ArgumentMetadatasays where the value came from (type), which property the decorator named (data) and the type the handler declared (metatype), which is the type the pipe must produce.- Pipes convert and check one argument; guards decide access, interceptors wrap the handler, middleware sees the raw request first, and services keep the rules that need data.
Terms in this lesson
- ArgumentMetadata
- What Nest passes to a pipe about the argument: type (body, query, param or custom), data (the name given to the decorator, if any), metatype (the declared type of the parameter) and, in Nest 12, schema.
- PipeTransform
- The interface every Nest pipe implements: one method, transform(value, metadata), whose return value replaces the handler's argument and whose thrown exception becomes the response. It may return a promise.
References
- NestJS documentation: Pipes (custom pipes, the transformation use case) (NestJS)
- NestJS documentation: Request lifecycle (NestJS)
- NestJS documentation: Guards (NestJS)
- NestJS documentation: Interceptors (NestJS)
- NestJS documentation: Exception filters (unrecognized exceptions) (NestJS)
- NestJS documentation: Migration guide (pipe transform signatures) (NestJS)
- NestJS v12.0.0 release notes (NestJS (GitHub))
- Error Handling Cheat Sheet (OWASP Foundation)
- JavaScript reference: Number (double-precision floating point) (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress