To convert a number to a string with leading zeros in TypeScript, turn it into a string with String() and call padStart(width, "0"). A number can’t store leading zeros, so the zeros only ever exist in the string you return:
const padded = String(2139).padStart(5, "0"); // "02139"
Every snippet here was compiled with TypeScript 7.0.2 and Node.js v22.22.2 in strict mode, and each output block is pasted straight from my console.
Below you’ll find padStart(), a safe helper for negative numbers, the compiler errors people hit, Intl.NumberFormat, the old slice trick, and real uses like times and invoice IDs.
Convert a number to a string with leading zeros using padStart()
padStart() adds characters to the front of a string until it reaches the target length. Convert the number first with String(n), n.toString() or a template literal; all three give the same text:
const zip: number = 2139; // Cambridge, MA
const padded: string = String(zip).padStart(5, "0");
console.log(padded);
const ids: number[] = [7, 42, 315, 12345, 987654];
for (const id of ids) {
console.log(`${id}`.padStart(5, "0"), "|", id.toString().padStart(5, "0").length, "chars");
}
Output:
02139
00007 | 5 chars
00042 | 5 chars
00315 | 5 chars
12345 | 5 chars
987654 | 6 chars

Notice the last line. When the number is already wider than the target, you get it back unchanged, so no digits are lost. If you need the basics of turning numbers into text first, see how to convert a number to a string in TypeScript.
Pad negative numbers and decimals with leading zeros
padStart() works on characters, not math. The minus sign and the decimal point count toward the width, so a plain call puts zeros in the wrong place. This helper pads the digits and puts the sign back:
console.log("negative: ", String(-7).padStart(3, "0"));
console.log("decimal: ", String(4.5).padStart(4, "0"));
function padNumber(value: number | bigint, width: number): string {
if (typeof value === "number" && !Number.isInteger(value)) {
throw new RangeError(`padNumber expects an integer, got ${value}`);
}
const negative = value < 0;
const digits = String(negative ? -value : value).padStart(width, "0");
return negative ? `-${digits}` : digits;
}
console.log("helper: ", padNumber(-7, 3));
console.log("bigint: ", padNumber(9007199254740993n, 20));
try {
padNumber(4.5, 4);
} catch (err) {
console.log("error: ", (err as Error).message);
}
Output:
negative: 0-7
decimal: 04.5
helper: -007
bigint: 00009007199254740993
error: padNumber expects an integer, got 4.5

padStart gives 0-7; the helper gives -007 and also accepts bigint values.I reject non-integers on purpose. Most leading-zero formats are IDs, ZIP codes and counters, and a decimal there usually means a bug upstream. Accepting number | bigint keeps big account numbers exact, since a plain number loses precision past 253.
Watch the width rule too: padStart(3) means three characters total, not three zeros. You can confirm the result with a TypeScript string length check.
Why does TypeScript say padStart does not exist on type number?
padStart() lives on String.prototype, so calling it straight on a number fails to compile in strict mode:
const account: number = 4021;
const padded = account.padStart(8, "0");
console.log(padded);
Output:
zeros_errors.ts(2,24): error TS2339: Property 'padStart' does not exist on type 'number'.

The fix is to convert first: String(account).padStart(8, "0"). If padStart still isn’t found on a string, your lib setting is older than ES2017:
const store: string = String(85);
console.log(store.padStart(4, "0"));
Output:
zeros_oldlib.ts(2,19): error TS2550: Property 'padStart' does not exist on type 'string'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2017' or later.
Set "target" or "lib" in tsconfig.json to es2017 or later and the error goes away. Every current browser and Node.js release supports it.
Add leading zeros with Intl.NumberFormat
Intl.NumberFormat has a minimumIntegerDigits option that pads the integer part for you. Turn off useGrouping, or 1234567 comes back with commas:
const fmt = new Intl.NumberFormat("en-US", {
minimumIntegerDigits: 6,
useGrouping: false,
});
console.log(fmt.format(4021));
console.log(fmt.format(-58));
console.log(fmt.format(1234567));
const money = new Intl.NumberFormat("en-US", {
minimumIntegerDigits: 3,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
console.log(money.format(7.5));
Output:
004021
-000058
1234567
007.50
It handles the minus sign correctly and can fix decimal places at the same time. The catch is speed and limits: build the formatter once and reuse it, and remember minimumIntegerDigits only goes up to 21.
Pad with slice() and repeat() (and why it can lose digits)
Before ES2017, people prepended a run of zeros and kept the last n characters. You’ll still see this in older codebases:
function padSlice(n: number, width: number): string {
return ("0".repeat(width) + n).slice(-width);
}
console.log(padSlice(42, 5));
console.log(padSlice(1234567, 5)); // too long: digits are lost
console.log(String(1234567).padStart(5, "0")); // padStart keeps every digit
Output:
00042
34567
1234567
The second line shows the bug: 1234567 silently becomes 34567. Keep this pattern only when you really want the last n digits; otherwise use padStart().
For trimming text on purpose, removing a substring in TypeScript covers the cleaner options.
Format times, dates and invoice numbers with leading zeros
Two-digit months, hours and minutes are the most common real use. A tiny two() helper covers dates, countdown timers and zero-padded invoice IDs:
const two = (n: number): string => String(n).padStart(2, "0");
const meeting = new Date(2026, 9, 14, 9, 5); // Oct 14, 2026, 9:05 AM local time
const stamp = `${meeting.getFullYear()}-${two(meeting.getMonth() + 1)}-${two(meeting.getDate())}`;
console.log("date:", stamp, "time:", `${two(meeting.getHours())}:${two(meeting.getMinutes())}`);
const seconds = 3725;
const clock = [Math.floor(seconds / 3600), Math.floor((seconds % 3600) / 60), seconds % 60].map(two).join(":");
console.log("timer:", clock);
const invoices: string[] = [10, 9, 105, 2].map((n) => `INV-2026-${String(n).padStart(5, "0")}`);
console.log("sorted:", [...invoices].sort());
console.log("unpadded:", ["INV-10", "INV-9", "INV-105", "INV-2"].sort());
Output:
date: 2026-10-14 time: 09:05
timer: 01:02:05
sorted: [
'INV-2026-00002',
'INV-2026-00009',
'INV-2026-00010',
'INV-2026-00105'
]
unpadded: [ 'INV-10', 'INV-105', 'INV-2', 'INV-9' ]

That sort result is the hidden win. String sorting compares character by character, so zero-padded IDs line up correctly without a custom comparator. I explain why in how to compare strings in TypeScript and sorting arrays in TypeScript.
For full date formatting, it’s often easier to use toLocaleDateString() with month: "2-digit". Getting today’s values is covered in how to get the current date in TypeScript.
Do leading zeros survive converting back to a number?
No. As soon as you call Number() or parseInt(), the zeros are gone, and turning it back into a string won’t restore them:
const zipText: string = "02139";
const zipNumber: number = Number(zipText);
console.log(zipNumber, typeof zipNumber);
console.log(String(zipNumber) === zipText);
interface Address {
city: string;
state: string;
zip: string; // keep ZIP codes as strings
}
const office: Address = { city: "Newark", state: "NJ", zip: String(7102).padStart(5, "0") };
console.log(office);
Output:
2139 number
false
{ city: 'Newark', state: 'NJ', zip: '07102' }
That’s why ZIP codes, phone numbers and account numbers belong in string fields. Only convert when you need math, and pad again for display. See converting a string to a number in TypeScript and checking if a string is a number for safe parsing.
You can’t dodge this by writing the zero into the source, either. TypeScript rejects a numeric literal with a leading zero:
const zip = 02139;
Output:
zeros_literal.ts(1,13): error TS1489: Decimals with leading zeros are not allowed.
Leading zero methods in TypeScript compared
| Method | Best for | Watch out for |
|---|---|---|
String(n).padStart(w, "0") | Most cases: IDs, ZIP codes, times | Minus sign counts toward the width |
padNumber() helper | Negative numbers and bigint | Decide how to treat decimals |
Intl.NumberFormat | Negatives plus fixed decimal places | Set useGrouping: false; max 21 digits |
("0".repeat(w) + n).slice(-w) | Keeping only the last n digits | Silently drops digits from long numbers |
Keep going with these string and number guides:
- Combine an array of strings with join() in TypeScript
- Remove spaces from a string in TypeScript
- Reverse a string in TypeScript
- Get the week number from a date in TypeScript
Frequently asked questions
How do I add leading zeros to a number in TypeScript?
Convert it to a string and pad it: String(n).padStart(5, "0"). The result is a string, because numbers can’t hold leading zeros.
Why does padStart put the zero before the minus sign?
It counts characters, so "-7" becomes "0-7". Pad the absolute value and add the sign back, or use Intl.NumberFormat with minimumIntegerDigits.
Does padStart cut off numbers that are too long?
No. If the string is already as long as the target width, padStart() returns it unchanged. The older slice(-n) trick is the one that cuts digits.
How do I fix “Property padStart does not exist on type number”?
Call it on a string: String(n).padStart(...). If the error mentions type string, set lib or target to ES2017 or later.
Should I store ZIP codes as numbers in TypeScript?
No. Store them as string. Converting "02139" to a number drops the leading zero for good.
Where is padStart documented?
See String.prototype.padStart() on MDN.

Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn