Engineering

Are You Using Your AI Copilot, or Is It Using You?

Are You Using Your AI Copilot, or Is It Using You?

You type a single comment: // Fetch user data from /api/v2/users and display it in a list. Before your fingers leave the keyboard, 30 lines of working code appear. You commit, push, move on to the next ticket. Productive? Yes. But what did you learn?

Nothing. You downloaded a pre-compiled binary for your brain. You have the output but zero insight into the source code of that knowledge. You didn't wrestle with the async nature of the request, you didn't discover how the library handles errors, and you didn't internalize the data structure. You just pressed tab.

I've been watching this happen on teams I work with, and honestly, I've caught myself doing it too. Recent studies confirm the suspicion: developers who offload the entire cognitive process to AI consistently perform worse on the underlying concepts. They don't get faster long-term because they never built the debugging muscles needed when the AI's perfect code collides with messy reality.

True expertise comes from inefficiency. From staring at a screen, tracing a bug through four layers of abstraction, building the intellectual scar tissue that stops you from making the same mistake twice.

Delegators vs. Collaborators

Here's the distinction that matters. There are two ways to use AI:

The Delegator treats the AI as a code vending machine. Provide a request, receive a finished product, paste it in. The thinking is outsourced. The goal is to close the ticket as fast as possible.

The Collaborator treats the AI as an infinitely patient pair programmer. They use it to explore, question, and build mental models. The goal is to understand the how and why, using the AI to accelerate that process.

An AI collaborator doesn't hand you answers. It helps you find them yourself. You drive the process, you ask the questions, and you keep intellectual ownership.

Two developers, same task

Let's make this real. Alex and Ben are both implementing a data-fetching component in React using SWR, a library neither has used before.

Alex: The Delegator

Alex opens his editor and types one prompt:

// Create a React component using TypeScript that fetches user data from
// 'https://api.example.com/users/1' with SWR.
// Handle loading and error states.
// Add a button to update the user's name to 'Alex' and revalidate.

Five seconds later, working code appears:

import useSWR, { useSWRConfig } from 'swr';
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
const fetcher = (url: string) => fetch(url).then(res => res.json());
 
function UserProfile() {
  const { data, error, isLoading } = useSWR<User>('https://api.example.com/users/1', fetcher);
  const { mutate } = useSWRConfig();
 
  const handleUpdateName = async () => {
    const userApiUrl = 'https://api.example.com/users/1';
    mutate(userApiUrl, (currentUser: any) => ({ ...currentUser, name: 'Alex' }), false);
 
    await fetch(userApiUrl, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Alex' }),
    });
 
    mutate(userApiUrl);
  };
 
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load</div>;
 
  return (
    <div>
      <h1>{data?.name}</h1>
      <p>{data?.email}</p>
      <button onClick={handleUpdateName}>Set Name to Alex</button>
    </div>
  );
}
 
export default UserProfile;

Alex copies it, sees it works, and closes the ticket. A week later, a bug report: the optimistic UI update flickers when the API call fails. Alex stares at that mutate function. He has no idea what the third parameter false means, or why there are two mutate calls. He doesn't understand the caching model. He's stuck.

Ben: The Collaborator

Ben doesn't ask for the entire solution. He has a conversation.

"I'm learning SWR. What's its core philosophy? How is it different from calling fetch inside a useEffect?"

The AI explains stale-while-revalidate, caching, request deduplication. Ben reads it and internalizes the concepts.

"Show me a minimal TypeScript example for just the fetching part. I want to understand the hook signature first."

The AI provides a focused snippet. Ben types it out himself, making small changes to feel the shape of the API.

"Now how do I handle mutations? I need a button that updates the user's name. Explain optimistic UI and how mutate works. What are its parameters?"

The AI breaks down mutate: local cache updates, the revalidation flag, the difference between bound and global mutate.

After 20 minutes, Ben has the same feature working. But when that same flicker bug comes in, he immediately knows to look at the mutate revalidation behavior and fixes it in minutes.

import useSWR from 'swr';
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
const fetcher = async (url: string): Promise<User> => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error('Data fetching failed');
  }
  return response.json();
};
 
function UserProfile({ userId }: { userId: number }) {
  const apiUrl = `https://api.example.com/users/${userId}`;
  const { data, error, isLoading } = useSWR<User, Error>(apiUrl, fetcher);
 
  if (isLoading) return <div>Loading profile...</div>;
  if (error) return <div>Error: {error.message}</div>;
 
  return (
    <div>
      <h1>{data?.name}</h1>
      <p>Email: {data?.email}</p>
    </div>
  );
}

The final code might look similar. The difference is entirely in the developer's head.

My personal rule

When I'm in learning mode (new library, new pattern, unfamiliar territory), the AI's role is strictly Socratic. I type the code myself, embrace the errors, and only turn to the AI to ask why something failed, or for a better mental model. I want the struggle.

When I'm in execution mode (boilerplate for a pattern I know cold), I let the AI do the heavy lifting. No point in manually typing a CRUD scaffold for the 50th time.

The distinction is simple: if I can't explain the code without the AI, I haven't learned it yet. And code I don't understand is code I can't debug.

The tool is powerful. Use it. But make sure you're the one doing the thinking.