Type Function Parameters in TypeScript
Every function parameter in TypeScript needs a type annotation. Learn the syntax for typing parameters, why inference does not work here, and how to handle destructured parameters.
To type function parameters in TypeScript, write a colon and the type after the parameter name. This tells TypeScript what kind of value the function expects to receive. Parameter annotations are the most important type annotations you write because, unlike variables, TypeScript cannot infer them from a surrounding value.
Here is the basic syntax:
function greet(name: string, age: number) {
return `${name} is ${age} years old.`;
}
console.log(greet("Alice", 30));
// Output: Alice is 30 years old.The colon and type after each parameter name are the annotations. The compiler checks every call to this function and reports errors when arguments do not match, so passing a string where a number is expected fails before the code ever runs:
greet("Alice", "thirty");The second argument is a string, but the parameter expects a number, so the compiler reports this error before the function ever runs:
Argument of type 'string' is not assignable to parameter of type 'number'.Why Parameters Need Annotations
When you declare a variable with an initial value, TypeScript infers the type from that value automatically, so you rarely need to write it yourself:
let message = "Hello";
// TypeScript infers: stringA function parameter has no initial value visible at the declaration site. The value comes from the caller, and the caller could be anywhere in the codebase. Without an annotation, TypeScript cannot guess the intended type:
function logValue(value) {
// 'value' is implicitly 'any'
console.log(value.toUpperCase());
}
logValue(42);
// Runtime error: value.toUpperCase is not a functionWith strict mode enabled, TypeScript reports an error on the untyped parameter instead of letting it slip through as any. Add the annotation to fix it:
function logValue(value: string) {
console.log(value.toUpperCase());
}
logValue(42);Calling this corrected function with a number now fails right at the call site instead of crashing later at runtime with a confusing message:
Argument of type 'number' is not assignable to parameter of type 'string'.The compiler catches the mistake at the call site, before the code runs. This is the core value of parameter annotations: they push type errors from deep inside the function body up to the call site, where the fix is obvious.
Multiple Parameters
A function with several parameters follows the same pattern. Each parameter gets its own annotation, separated by commas:
function createUser(name: string, age: number, isAdmin: boolean) {
return { name, age, role: isAdmin ? "admin" : "user" };
}
const user = createUser("Bob", 25, false);
console.log(user);
// Output: { name: "Bob", age: 25, role: "user" }The order of parameters matters. The caller must provide arguments in the same order as the parameters are declared, and TypeScript checks each position independently. Swapping the first two arguments produces this error:
createUser(25, "Bob", false);The first argument is a number, but the first parameter expects a string, so TypeScript flags the mismatch right away instead of waiting for a runtime failure:
Argument of type 'number' is not assignable to parameter of type 'string'.The error points to the first parameter because 25 is a number but the parameter expects a string. TypeScript caught the swapped arguments even though the third argument still matched the boolean parameter correctly.
Destructured Parameters
When a function receives an object, you can destructure it directly in the parameter list. The type annotation goes after the entire destructuring pattern, not on each property:
function printUser({ name, age }: { name: string; age: number }) {
console.log(`${name}, ${age}`);
}
printUser({ name: "Alice", age: 30 });
// Output: Alice, 30The inline object type describes the shape of the object the caller must pass. The destructuring pulls out each property as a local variable, and TypeScript knows its type from the object type.
For a complex object type, use a type alias to keep the function signature readable:
type User = {
name: string;
age: number;
email: string;
};
function sendWelcomeEmail({ name, email }: User) {
console.log(`Sending welcome email to ${name} at ${email}`);
}Calling the function still requires every property the User type declares, even though the function body only reads two of them:
sendWelcomeEmail({ name: "Bob", age: 25, email: "bob@example.com" });
// Output: Sending welcome email to Bob at bob@example.comThe alias makes the intent clear and keeps the function signature short. TypeScript still checks that the caller provides an object matching all the required properties.
Union and Literal Parameter Types
A parameter can accept more than one type when the function handles both cases:
function formatValue(value: string | number): string {
if (typeof value === "number") {
return value.toFixed(2);
}
return value.trim();
}
console.log(formatValue(42));
console.log(formatValue(" hello "));
// Output: 42.00
// Output: helloThe union string | number tells callers that either type is valid. Inside the function, you must narrow the type before using methods that exist on only one member of the union. For more on narrowing, see Type narrowing basics in TypeScript.
A literal type restricts a parameter to exact values:
function setAlignment(direction: "left" | "right" | "center") {
console.log(`Aligning to ${direction}`);
}
setAlignment("left"); // OK
setAlignment("top");The first call matches one of the three literal strings, so it compiles. The second call passes a string that is not in the allowed set, so TypeScript rejects it with this error:
Argument of type '"top"' is not assignable to parameter of type '"left" | "right" | "center"'.Literal parameter types work well for options, modes, and configuration strings. They prevent typos at the call site and document the valid choices.
Objects and Arrays as Parameters
Object and array parameters work the same way as primitive parameters. The annotation describes the shape:
function addTask(tasks: string[], newTask: string): string[] {
return [...tasks, newTask];
}
const updated = addTask(["read", "write"], "review");
console.log(updated);
// Output: ["read", "write", "review"]The tasks parameter is typed as an array of strings, so the caller must pass an array of strings. The function returns a new array instead of mutating the original, which is the safer pattern in TypeScript.
Objects are passed by reference. If the function mutates the object, the caller sees the change. TypeScript does not prevent this at the type level unless you use readonly modifiers:
function updateAge(user: { name: string; age: number }, newAge: number) {
user.age = newAge;
}
const alice = { name: "Alice", age: 30 };
updateAge(alice, 31);
console.log(alice.age);
// Output: 31The mutation is visible because alice and user refer to the same object. TypeScript's type system tracks the property types but does not enforce immutability by default. Use readonly properties or return a new object when you want to prevent mutation.
Common Mistakes
Forgetting to annotate a parameter. In strict mode, an untyped parameter produces an error. Even outside strict mode, it silently becomes any, disabling all type checking for that parameter and its uses.
Typing each destructured property individually. The annotation goes after the whole pattern, not on each property inside the braces:
// Wrong: annotating each property inside the pattern
function greet({ name: string, age: number }) {
// This renames name to string and age to number, not what you want
}
// Correct: annotate the whole object
function greet({ name, age }: { name: string; age: number }) {
// name is string, age is number
}Confusing JavaScript default values with type annotations. A default value does not replace a type annotation. TypeScript infers the type from the default, but an explicit annotation is clearer and catches mistakes when the default does not match the intent.
For the next steps, see optional parameters in TypeScript to learn how to make parameters optional, and default parameters in TypeScript for setting fallback values.
Rune AI
Key Insights
- Annotate every function parameter with
param: Typesyntax. - TypeScript cannot infer parameter types, so annotations are mandatory for type safety.
- Without annotations, parameters get the implicit any type and lose all checking.
- Destructured parameters need the object type after the pattern, not on each property.
- Object and array parameters are passed by reference, just like in JavaScript.
Frequently Asked Questions
Do I always need to annotate function parameters?
Can I use union types for a parameter?
How do I type a destructured parameter?
Conclusion
Type annotations on function parameters are the most important annotations you write. Unlike variables, parameters have no value to infer from, so every parameter needs an explicit type. The syntax is param: Type. For destructured parameters, place the object type after the destructuring braces. Each annotation protects one function from receiving the wrong data.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.