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.

5 min read

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:

typescripttypescript
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:

typescripttypescript
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:

texttext
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:

typescripttypescript
let message = "Hello";
// TypeScript infers: string

A 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:

typescripttypescript
function logValue(value) {
  // 'value' is implicitly 'any'
  console.log(value.toUpperCase());
}
 
logValue(42);
// Runtime error: value.toUpperCase is not a function

With 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:

typescripttypescript
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:

texttext
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:

typescripttypescript
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:

typescripttypescript
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:

texttext
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:

typescripttypescript
function printUser({ name, age }: { name: string; age: number }) {
  console.log(`${name}, ${age}`);
}
 
printUser({ name: "Alice", age: 30 });
// Output: Alice, 30

The 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:

typescripttypescript
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:

typescripttypescript
sendWelcomeEmail({ name: "Bob", age: 25, email: "bob@example.com" });
// Output: Sending welcome email to Bob at bob@example.com

The 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:

typescripttypescript
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: hello

The 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:

typescripttypescript
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:

texttext
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:

typescripttypescript
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:

typescripttypescript
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: 31

The 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:

typescripttypescript
// 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

Rune AI

Key Insights

  • Annotate every function parameter with param: Type syntax.
  • 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.
RunePowered by Rune AI

Frequently Asked Questions

Do I always need to annotate function parameters?

Yes, in strict mode. Unlike variables, TypeScript cannot infer parameter types because the function declaration does not show what value will be passed. Without annotations, parameters get the implicit any type, which disables type checking for that parameter.

Can I use union types for a parameter?

Yes. Write the union after the colon, for example (value: string | number). The function body must narrow the type before using methods specific to one member of the union.

How do I type a destructured parameter?

Place the type annotation after the destructuring pattern: function greet({ name, age }: { name: string; age: number }). The type describes the full object, not the individual properties.

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.