How to Convert Number to String with Leading Zeros in TypeScript

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
Command Prompt output of TypeScript padStart turning the numbers 2139, 7, 42 and 315 into five-character strings with leading zeros
The ZIP code comes back as 02139, and 987654 stays six characters because padStart never cuts anything off.

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
Command Prompt output showing padStart giving 0-7 for a negative number and a TypeScript padNumber helper returning -007 and a padded bigint
Plain 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'.
Command Prompt showing the tsc error TS2339: Property padStart does not exist on type number
The compiler catches the mistake before the code ever runs.

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' ]
Command Prompt output of TypeScript formatting a 2026 date and time with leading zeros, an HH:MM:SS timer, and sorting padded invoice numbers
Padded invoice numbers sort in the right order as plain strings; the unpadded ones put INV-10 before INV-2.

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

MethodBest forWatch out for
String(n).padStart(w, "0")Most cases: IDs, ZIP codes, timesMinus sign counts toward the width
padNumber() helperNegative numbers and bigintDecide how to treat decimals
Intl.NumberFormatNegatives plus fixed decimal placesSet useGrouping: false; max 21 digits
("0".repeat(w) + n).slice(-w)Keeping only the last n digitsSilently drops digits from long numbers

Keep going with these string and number guides:

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?