Type Existing JavaScript Functions
Learn how to add TypeScript type annotations to existing JavaScript functions. Read the function body to infer types, add parameter and return types, and handle tricky patterns.
You type JavaScript functions by reading what each one does and adding TypeScript annotations so the compiler can verify it. You do not need to rewrite the logic. You add types around the code that is already there.
This is the most common task during a JavaScript-to-TypeScript migration. Every function you type makes the rest of the codebase easier to convert, because callers get immediate feedback when they misuse it.
Read the Function to Find Its Types
Before adding annotations, read the function body and ask three questions:
- What types does this function expect for each parameter?
- What does it return?
- Does it ever return nothing, throw, or return different types?
Here is a typical JavaScript function:
function formatPrice(price, currency) {
if (currency === "USD") {
return "$" + price.toFixed(2);
}
if (currency === "EUR") {
return "€" + price.toFixed(2);
}
return price.toFixed(2) + " " + currency;
}Reading the body tells you:
- price must be a number, since it calls toFixed, which only exists on numbers.
- currency must be a string, since it is compared with strict equality against string literals.
- The function always returns a string.
Add those types:
function formatPrice(price: number, currency: string): string {
if (currency === "USD") {
return "$" + price.toFixed(2);
}
if (currency === "EUR") {
return "€" + price.toFixed(2);
}
return price.toFixed(2) + " " + currency;
}The logic did not change. Only the signature changed.
Start With the Simplest Functions
Begin with functions that have a single clear purpose. These are the easiest to type and give you the most immediate benefit:
// Before
function isAdult(age) {
return age >= 18;
}
// After
function isAdult(age: number): boolean {
return age >= 18;
}The body tells you everything you need here: age is compared against a number, and the comparison itself produces a boolean. Functions like this take only a few seconds to type once you get used to reading the body first.
Handle Optional Parameters
JavaScript functions often have parameters that may or may not be passed. Look for default values or checks against undefined:
function greet(name, greeting) {
if (greeting === undefined) {
return "Hello, " + name;
}
return greeting + ", " + name;
}The check against undefined is the signal: greeting is optional, so mark it with a question mark after the parameter name instead of requiring every caller to pass it.
function greet(name: string, greeting?: string): string {
if (greeting === undefined) {
return "Hello, " + name;
}
return greeting + ", " + name;
}If the function uses a default value in the parameter list instead of a check, TypeScript infers the parameter's type directly from that default value, so no separate annotation is needed:
function createUser(name: string, role = "user") {
return { name, role };
}TypeScript infers role as a string from the default value "user". No explicit annotation needed.
Replace the arguments Object
The arguments object is common in older JavaScript:
// Before
function logAll() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}It has no useful type on its own, and it does not exist inside arrow functions at all, so migrations usually replace it with a rest parameter instead:
// After
function logAll(...messages: string[]): void {
for (const message of messages) {
console.log(message);
}
}The rest parameter collects all arguments into a typed array. The function body becomes cleaner because you can use array methods instead of index-based loops.
If the function accepts mixed types, use a union:
function logAll(...values: (string | number)[]): void {
for (const value of values) {
console.log(value);
}
}Handle Functions That Return Different Types
Some JavaScript functions return different types depending on the input. This is common in parsing and data transformation:
function parseId(id) {
if (typeof id === "number") {
return id;
}
return parseInt(id, 10);
}The return type is always a number, even though the input varies: a plain number passes straight through, and a string gets parsed into one. That kind of branching return value is still a single type, so the annotation stays simple:
function parseId(id: string | number): number {
if (typeof id === "number") {
return id;
}
return parseInt(id, 10);
}When a function genuinely returns different types depending on the input it receives, a single type cannot describe the result honestly, so reach for a union return type instead:
function getConfig(key) {
if (key === "port") {
return 3000;
}
if (key === "host") {
return "localhost";
}
return null;
}Each branch returns a different kind of value, and there is no single type that covers all three without lying about the shape. A union return type lists every real possibility so callers still get accurate checking:
function getConfig(key: string): number | string | null {
if (key === "port") {
return 3000;
}
if (key === "host") {
return "localhost";
}
return null;
}Use Function Overloads for Complex Signatures
When a function behaves differently based on argument types, use overloads. This function returns a string when given a string, and a number when given a number:
function process(input) {
if (typeof input === "string") {
return input.toUpperCase();
}
return input * 2;
}A plain union return type would technically be correct here, but it would force every caller to narrow the result even when they already know which branch ran. Overloads let TypeScript track the connection between the input and output types instead:
function process(input: string): string;
function process(input: number): number;
function process(input: string | number): string | number {
if (typeof input === "string") {
return input.toUpperCase();
}
return input * 2;
}The overload signatures (the first two lines) tell TypeScript the precise relationship between input and output types. The implementation signature uses the union. Callers get the correct return type based on what they pass in.
For more on overloads, see Function Overloads in TypeScript.
Type Callback Parameters
JavaScript functions that accept callbacks need types for the callback signature:
function fetchData(url, onSuccess, onError) {
fetch(url)
.then(response => response.json())
.then(data => onSuccess(data))
.catch(err => onError(err));
}Each callback parameter needs its own function type, written as parameter names followed by a return type, so that editor autocomplete and error checking work inside the callback bodies too:
function fetchData(
url: string,
onSuccess: (data: unknown) => void,
onError: (error: Error) => void
): void {
fetch(url)
.then(response => response.json())
.then(data => onSuccess(data))
.catch(err => onError(err));
}For a deeper dive into callback types, see Type Callback Functions in TypeScript.
Let TypeScript Infer Return Types
You do not always need to write the return type. If the function body is straightforward, TypeScript infers it:
function add(a: number, b: number) {
return a + b;
}Hover over add in your editor, and TypeScript shows the inferred return type is number. You can add the annotation for clarity, but it is not required.
Explicit return types are most valuable on public API functions where you want to guarantee the contract does not accidentally change.
Handle Functions With Side Effects
Functions that modify external state and return nothing should be typed with void:
function updateUser(user, changes) {
Object.assign(user, changes);
user.updatedAt = Date.now();
}The function mutates the object it receives instead of returning a new one, so the return type is void. The parameter type only needs to describe the property this function actually touches:
function updateUser(user: { updatedAt?: number }, changes: object): void {
Object.assign(user, changes);
user.updatedAt = Date.now();
}The void type tells callers that this function does work but does not produce a value to use.
Common Patterns and Their Types
| JavaScript pattern | TypeScript type |
|---|---|
return; (no value) | void |
return someCondition ? "a" : 1; | string or number |
throw new Error("msg") | never, no need to annotate |
if (!value) return; | void, early return guard |
setTimeout(() => {...}, 1000) | callback has a void return |
arr.find(x => x.id === id) | the element type or undefined |
JSON.parse(str) | any, narrow it with a type annotation |
Rune AI
Key Insights
- Read the function body to understand what types it expects and returns.
- Start with required parameters, then add optional ones using ?.
- Replace the arguments object with rest parameters (...args).
- Use function overloads when a function handles multiple argument shapes.
- Type callbacks with their full signature for better editor support.
- Let TypeScript infer return types initially, then add explicit annotations.
Frequently Asked Questions
How do I type a function that does many different things based on its arguments?
What if I cannot figure out the return type?
Should I type every function parameter immediately?
Conclusion
Typing existing JavaScript functions is a process of reading what the function does and translating that into TypeScript annotations. Start with the function signature, handle dynamic argument patterns, and add return types. Each typed function makes the rest of the codebase easier to convert.
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.