The error argument of type ‘number’ is not assignable to parameter of type ‘string’ (TS2345) means a number went into a string parameter. Convert it with String(), or widen the parameter to string | number:
formatZip(String(zipCode)); // was formatZip(zipCode)
I ran all of this on TypeScript 7.0.2 and Node.js v22.22.2 in strict mode, so the error text below is what tsc actually printed on my machine.
In this tutorial, I’ll reproduce the error in both directions, show the assignment version (TS2322, type ‘string’ is not assignable to type ‘number’), and walk through each fix: conversions, NaN checks, unions, overloads, generics, form inputs and JSON data.
What causes TS2345: argument of type ‘number’ is not assignable to parameter of type ‘string’?
TypeScript checks each argument against the parameter’s declared type. A ZIP code stored as a number, a price passed to parseFloat(), and a count saved with localStorage.setItem() all trigger it, because each of those parameters is typed string:
function formatZip(zip: string): string {
return zip.padStart(5, "0");
}
const zipCode = 2134; // Boston, MA (02134)
formatZip(zipCode);
const price = 19.99;
parseFloat(price);
localStorage.setItem("cartCount", 3);
tsc output:
arg_error.ts(6,11): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
arg_error.ts(9,12): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
arg_error.ts(11,35): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

Notice the parseFloat(price) case. The value is already a number, so you don’t need parseFloat at all; if you wanted rounding, use toFixed() as shown in rounding to 2 decimals in TypeScript.
The .ts(2345) suffix you see in VS Code is the same error number. TypeScript’s codes don’t change between editors, so searching for TS2345 always lands on this family of argument errors.
Running this code on an older Node.js version? Read my guide on fixing ReferenceError: fetch is not defined in TypeScript first.
Fix it: convert the number to a string
If the function genuinely needs text, convert at the call site. String(), toString() and a template literal all give the same result for a normal number:
function formatZip(zip: string): string {
return zip.padStart(5, "0");
}
const zipCode = 2134;
console.log("String(): ", formatZip(String(zipCode)));
console.log("toString(): ", formatZip(zipCode.toString()));
console.log("template: ", formatZip(`${zipCode}`));
const total = 1249.5;
console.log("toFixed(2): ", "$" + total.toFixed(2));
console.log("toLocaleString: ", total.toLocaleString("en-US", { style: "currency", currency: "USD" }));
const missing: number | null = null;
console.log("String(null): ", String(missing));
Output:
String(): 02134
toString(): 02134
template: 02134
toFixed(2): $1249.50
toLocaleString: $1,249.50
String(null): null
I reach for String() by default because it also handles null and undefined without throwing, while missing.toString() would crash. For leading zeros and other padding tricks, see converting a number to a string with leading zeros.
When the string is for display, format it instead of plain converting. toLocaleString() with currency: "USD" adds the dollar sign and commas; more patterns are in formatting numbers with commas in TypeScript.
Type ‘string’ is not assignable to type ‘number’ (TS2322)
The reverse problem is even more common, because so much data arrives as text. When you assign a string to a number variable, TypeScript reports TS2322; when you pass it as an argument, it’s TS2345 again with the types swapped:
let orderTotal: number = 0;
orderTotal = "149.99";
const input = document.querySelector<HTMLInputElement>("#qty")!;
const qty: number = input.value;
function getItemById(id: number): string {
return `Item ${id}`;
}
const params = new URLSearchParams("?itemId=42");
getItemById(params.get("itemId"));
Math.round("4.5");
tsc output:
assign_error.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.
assign_error.ts(5,7): error TS2322: Type 'string' is not assignable to type 'number'.
assign_error.ts(11,13): error TS2345: Argument of type 'string | null' is not assignable to parameter of type 'number'.
Type 'null' is not assignable to type 'number'.
assign_error.ts(13,12): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

params.get() returns string | null, so both parts must be handled.Here’s the rule of thumb I use: TS2322 is about a variable, property or return value, and TS2345 is about a function argument. The fix is the same, though. Convert the value or correct the declared type.
The getItemById(params.get("itemId")) line is exactly the SharePoint Framework (SPFx) case. An ID read from a query string or a jQuery val() call is text, but list methods like getById() expect a number.
How do I convert a string to a number in TypeScript?
You have four tools, and they disagree on messy input. This table runs each one on the same strings so you can pick the right one:
const samples = ["42", "19.99", " 7 ", "12px", "", "abc", "0x1A"];
console.log("value Number parseInt parseFloat unary+");
for (const s of samples) {
const label = JSON.stringify(s).padEnd(9);
const cols = [Number(s), parseInt(s, 10), parseFloat(s), +s].map((n) => String(n).padEnd(8));
console.log(label, cols.join(" "));
}
Output:
value Number parseInt parseFloat unary+
"42" 42 42 42 42
"19.99" 19.99 19 19.99 19.99
" 7 " 7 7 7 7
"12px" NaN 12 12 NaN
"" 0 NaN NaN 0
"abc" NaN NaN NaN NaN
"0x1A" 26 0 0 26

Number() turns an empty string into 0 and rejects “12px”; parseInt keeps the leading 12.Use Number() or unary + when the whole string must be numeric, and parseInt(s, 10) or parseFloat when you want the leading number from text like “12px”. The full how-to lives in convert a string to a number in TypeScript.
Always pass the radix 10 to parseInt. For decimal prices, converting a string to a double covers the precision side.
Check for NaN before you pass the number on
Conversion removes the compile error, but it can’t make bad input valid. A small helper that returns number | null forces every caller to deal with the failure case:
function toNumber(text: string | null): number | null {
if (text === null || text.trim() === "") return null;
const n = Number(text);
return Number.isNaN(n) ? null : n;
}
function getItemById(id: number): string {
return `Item ${id}`;
}
const params = new URLSearchParams("?itemId=42&page=abc");
for (const key of ["itemId", "page", "sort"]) {
const id = toNumber(params.get(key));
console.log(key.padEnd(7), id === null ? "not a number, skipped" : getItemById(id));
}
console.log("isNaN('abc'): ", isNaN("abc" as unknown as number));
console.log("Number.isNaN('abc'): ", Number.isNaN("abc"));
Output:
itemId Item 42
page not a number, skipped
sort not a number, skipped
isNaN('abc'): true
Number.isNaN('abc'): false
Prefer Number.isNaN() over the global isNaN(). The global one coerces first, so it says true for the string “abc”, while Number.isNaN only returns true for the actual NaN value. More validation patterns are in checking if a string is a number in TypeScript.
I also treat an empty or whitespace-only string as missing, because Number("") is 0, and a silent 0 is worse than an error.
Accept both types with a union, overloads or generics
Sometimes the function should take either type. Don’t silence the error with as any; describe the real contract instead:
// 1) Accept both and narrow with typeof
function formatZip(zip: string | number): string {
const text = typeof zip === "number" ? String(zip) : zip;
return text.padStart(5, "0");
}
console.log(formatZip(2134), formatZip("60601"));
// 2) Overloads: number in -> number out, string in -> string out
function double(value: number): number;
function double(value: string): string;
function double(value: number | string): number | string {
return typeof value === "number" ? value * 2 : value.repeat(2);
}
const a = double(21); // a: number
const b = double("ab"); // b: string
console.log(a, b);
// 3) Generics: the type comes from the argument
function firstItem<T>(items: T[]): T {
return items[0];
}
const firstPrice = firstItem([19.99, 5.25]); // number
const firstCity = firstItem(["Austin", "Denver"]); // string
console.log(firstPrice.toFixed(2), firstCity.toUpperCase());
Output:
02134 60601
42 abab
19.99 AUSTIN
The union with typeof narrowing is the simplest. Overloads give callers a precise return type, and generics let the argument decide the type.
If you’re unsure what a value is at runtime, checking the type of a variable shows the narrowing options, and the is keyword covers custom type guards.
Generics also work with arrow functions; the syntax is in generic arrow functions in TypeScript.
Form inputs, JSON and API data: where strings sneak in
HTMLInputElement.value is always a string, even for type="number" inputs. Use Number(input.value) or input.valueAsNumber. JSON is trickier, because JSON.parse() returns any and hides the problem completely:
interface ApiProduct {
name: string;
price: string; // the API sends "19.99", not 19.99
}
const body = '[{"name":"Mug","price":"19.99"},{"name&qu