The Situation

See if this sounds familiar: you’re passing functions around like candy in your React app, but when TypeScript throws a tantrum, you start hacking with any or begging Google for help. Let’s fix that. You’ll learn how to type function parameters like someone who knows what they’re doing and not someone duct-taping their code.

This is our agenda for today,

  1. Typing basic functions in TypeScript: Return types, parameters, and the bare minimum.
  2. Typing event handlers in React properly: No more any, no guessing. Use React.MouseEvent.
  3. Passing parameters to event handlers (the right way): Fixing the classic onClick={handleClick(5)} mistake.
  4. Combining custom params with event objects: (e) => handleClick(e, id) and how to type it cleanly.
  5. Passing data from parent to child: Props with both object and destructuring styles.
  6. Child components calling parent callbacks: onSelect, lifting state, and typing the callback.
  7. Handling lists with dynamic params and callbacks: Avoiding inline functions that wreck performance.
  8. Passing multiple parameters in callback props: Positional args vs grouping into objects.
  9. Defining reusable function types across files: No more repeating inline types. One type, reuse everywhere.
  10. Using useCallback properly (only when you should): No overkill. Just the real use case.
  11. Passing parameters into custom hooks: How to write hooks like useUser(id) and why simple is better.
  12. Lifting handlers across multiple layers: Flexible design: passing just the data, not hardcoded logic.

1. Before You Write Another Callback

Let’s kick it off with the classic:

function add(a: number, b: number): number{
    return a + b;
}

console.log(add(5,5)); // Output: 10

Yeah, that’s basic. You already know that. But now you’re in React, and suddenly:

export default function functions() {
    return (
        <button onClick={handleClick}> Click me</button>
    );
}

// Error: Cannot find name 'handleClick'.ts

And you’re like, how do I type handleClick?

2. Typing Event Handlers in React

TypeScript is picky with events, and it should be. Don’t guess the type. Use the built-in ones.

export default function SubmitButton() {

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    console.log(e.currentTarget);
  }

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

See, now it works.

 Typing Event Handlers in React

People try to pass data through components or event handlers and end up with spaghetti logic. You need to pass values to functions. You need to do it cleanly. And TypeScript needs to be happy. That’s it.

Let’s walk through how to pass parameters in every common React situation.

3. Passing Parameters to Event Handlers

Rookie Mistake, calling the function immediately during render.

function handleClick(id: number) {
    console.log("Clicked id: ", id);
}

<button onClick={handleClick(5)}>Click me</button>

// Error: Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'

// So what you’re effectively doing is:
<button onClick={undefined}>Click me</button>

So, TS is saying, you gave me something that runs immediately (and returns nothing, void), but I was expecting a function to call later when the button is clicked.

Let's fix that by wrapping in an Arrow Function.

<button onClick={() => handleClick(5)}>Click me</button>

function handleClick(id: number) {
    console.log("Clicked id: ", id);
}

Arrow Function

4. Event Parameter + Custom Data

<button onClick={(e) => handleClick(e, 5)}>Click me</button>

function handleClick(e: React.MouseEvent, id: number) {
    console.log(e.currentTarget);
    console.log("Clicked id: ", id);
}

You want access to the event and your custom data? Just pass both. TypeScript will figure out the event type if your handler is explicit.

Event Parameter + Custom Data

5. Passing Data to Child Components

You want to pass a parameter (like id) from the parent to the child, and use it in the child (e.g., in a button click).

Parent component

//Parent.tsx

import Child from "./Child";

export default function Parent(){
    return (
        <Child id={1} />
    );
}

Child component

1. Accessing parameters using props: keeps the full object and accesses its properties later

//Child.tsx

type childProps = {
    id: number
}

//With Props
export default function Child(props: childProps){

    function handleClick(){
        console.log("Clicked ID:", props.id);
    }

    return(
        <button onClick={ () => handleClick() }>Click me</button>
    );
}

2. Using Destructuring: Extracts values directly from an object:

//Child.tsx

type childProps = {
    id: number
}

//With Destructuring
export default function Child({id}: childProps){

    function handleClick(){
        console.log("Clicked ID:", id);
    }

    return(
        <button onClick={ () => handleClick() }>Click me</button>
    );
}

Using Destructuring

6. Handling Child Events in the Parent

When building reusable components, it's common for a child component to notify its parent about something that happened, like a button click. This is called “lifting state up” or event delegation.

Child component

//Child.tsx

type childProps = {
    id: number
    onSelect: (id: number) => void;
}


export default function Child({id, onSelect}: childProps){

    return(
        <button onClick={ () => onSelect(id)}>Click me</button>
    );
}

Parent component

//Parent.tsx

import Child from "./Child";

export default function Parent(){

function handleItemClick(itemId: number) {
    console.log("Child clicked:", itemId);
  }

    return (
        <Child id = {1} onSelect={handleItemClick} />
    );
}

7. List Rendering with Dynamic Params (Parent-Child)

When rendering a list of components dynamically, pass relevant data (like id and name) as props:

items.map((item) => (
          <Item key={item.id} 
                id={item.id} 
                name={item.name} 
                onSelect={handliClick} /> ))

Full version of Parent component

//Parent component

import Item from "./Item"

type Item = {
    id: number;
    name: string;
}

const items: Item[] = [
    { id: 1, name: "Buy Car" },
    { id: 2, name: "Buy House" }
]

export default function Parent() {
    function handliClick(id : number, name : string) {
        console.log(`Selected Id, ${id}, item: ${name}`);
    }

    return (
        <div>
            {
                items.map((item) => (<Item key={item.id} id={item.id} name={item.name} onSelect={handliClick} />))
            }
        </div>
    );
}

Child component

//Item.tsx : Child component

interface ItemProps {
    id: number;
    name: string;
    onSelect: (id: number, name: string) => void;
}

export default function Item({id, name, onSelect} : ItemProps) {
    return (
        <button onClick={() => onSelect(id, name)}> {name} </button>
    );
}

Mistakes to avoid when working with a list

Avoid using arrow functions for events while working with a list.

<Item ... 
       onSelect={() => handliClick(item.id)} />

Every time this component renders, a new anonymous function (() => handliClick(item.id)) is created for every item.

This causes

Instead, pass id as a prop and move the logic inside the child, like we did above.

Let's see the output

8. Multiple Parameters in Callback Props

As we saw above as well, sometimes your callback needs to accept multiple pieces of information. Here's another example where the callback receives two parameters: an ID and a status.

interface Props {
  onAction: (id: number, status: "Active" | "Inactive") => string;
}

const MultiArgs = ({ onAction }: Props) => {
  return (
    <button onClick={() => onAction(123, "Active")}>
            Action
    </button>
  );
};

export default MultiArgs;

8.1 Grouping Parameters into a Single Object

As your parameters grow, passing multiple values can get messy. Instead, it’s better to group related parameters into a single object with a clear type.

interface ActionData {
    id: number; 
    status: "Active" | "Inactive";
}

interface Props {
    onAction: ( actionArgs : ActionData) => string;
}

const MultiArgs = ({ onAction }: Props) => {
    return (
        <button onClick={() => onAction( { id: 123, status: "Active" } )}>
            Action
        </button>
    );
};

export default MultiArgs;

Now we're able to pass arguments as an object, and this is easy to scale.

9. Define Reusable Function Types for Callback Props

Problem: When passing functions (callbacks) as props to components in TypeScript, it's common to inline the function types like this:

interface Props{
    onSubmit : (data: { id: number, email: string }) => void;
}

This works, but over time:

The Solution: Use a Named Function Type:

Instead of repeating the inline shape every time, define a named function type once and reuse it:

1. Define type

type submitHandler = (args: { id: number, email: string }) => void;

2. Then plug it into your props

interface Props{
    onSubmit : submitHandler
}

Full Example

// types.ts
export type SubmitHandler = (data: { id: number; email: string }) => void;


// Child: ReuseFuncTypes .tsx
import type { SubmitHandler } from './types';

interface Props{
    onSubmit : SubmitHandler
}

export default function ReuseFuncTypes({ onSubmit }: Props) {
  const handleClick = () => {
    onSubmit({ id: 1, email: "[email protected]" });
  };

  return <button onClick={handleClick}>Submit</button>;
}

//Parent
import ReuseFuncTypes from "./ReuseFuncTypes"
import type { SubmitHandler } from './types';

export default function Parent() {

    const handleSubmit: SubmitHandler = (data) => {
        console.log("Form submitted:", data);
    };

    return (
        <div>
            {
                <ReuseFuncTypes onSubmit={handleSubmit}></ReuseFuncTypes>
            }
        </div>
    );
}

Output

Output

10. useCallback, When You Actually Need It

const handleSelect = useCallback((id: string) => {
  console.log('Selected:', id);
}, []);

Use it when

11. Parameters in Custom Hooks

function useUser(id: string) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    fetch(`/api/users/${id}`)
      .then((res) => res.json())
      .then(setUser);
  }, [id]);

  return user;
}

Pass ID directly. No need to be clever with objects or destructuring unless you're batching.

12. Lifting Handlers Across Components

You don’t need to pass a full handler with baked-in data.

<Parent onAction={handleGlobalAction} />

const Child = ({ id, onAction }: { id: string; onAction: (id: string) => void }) => {
  return <button onClick={() => onAction(id)}>Go</button>;
};

You get flexible components. And your parent doesn’t care how deep the call goes.

Wrap Up

If you're serious about building React apps with TypeScript, you can’t afford to wing function parameters. Whether it's handling clicks, lifting callbacks, or passing data cleanly, it all comes down to writing functions that are clear, typed, and reusable. Stop guessing, stop hacking, and start typing like you mean it.