back

Why people say TypeScript documents itself

While scrolling twitter, I saw a post about Cerebras which is a AI inference provider. They did a post about 10 rules when using AI agents to get the most of them. One of the rules stood out to me was this one: “Use firm language: “must” and “strictly” > soft suggestions”

Yeah one of my big problem is that I always say “please” or “can you” to my agent which I already knew wasn’t ideal but seeing that post made me want to do something to fix this bad habit.

My main coding agent is Opencode which has a plugin system so I decided I would create a plugin that would detect soft language and correct me. Here’s the gist for it.

Anyway while trying to figure out how make the plugin, I used a combination of the docs and types and I wanted to document the process with how and why since when I first started with TypeScript, I always heard people say it documents itself but didn’t quite understand why.

For those of you that don’t know, your code editor has a neat feature called go to definition. It allows you to go to the defition of a TypeScript type and is the main documentation people talk about when they say the phrase.

First thing I did, was use the structure of this plugin and copy it inside my editor. If you hover over the experimental.session.compacting right click of use whatever keyboard keybind you have setup, you will be taken to exact definition of that hook. If you scroll up a bit, you’ll see that you are that this string literal is part of the Hooks interface. Looking at the available hooks, I saw one called chat.message which looked perfect for my use case.

The definition of that hook is this

  "chat.message"?: (input: {
      sessionID: string;
      agent?: string;
      model?: {
          providerID: string;
          modelID: string;
      };
      messageID?: string;
      variant?: string;
  }, output: {
      message: UserMessage;
      parts: Part[];
  }) => Promise<void>;

So it takes an input which isn’t really what we care about but return an object containing a UserMessage and an array of Part If we look at the definition of UserMessage by going to it’s definition again we see this

export type UserMessage = {
    id: string;
    sessionID: string;
    role: "user";
    time: {
        created: number;
    };
    summary?: {
        title?: string;
        body?: string;
        diffs: Array<FileDiff>;
    };
    agent: string;
    model: {
        providerID: string;
        modelID: string;
    };
    system?: string;
    tools?: {
        [key: string]: boolean;
    };
};

Yeah not really interesting for what we are trying to do so I looked at the definition of Part

export type Part = TextPart | {
    id: string;
    sessionID: string;
    messageID: string;
    type: "subtask";
    prompt: string;
    description: string;
    agent: string;
} | ReasoningPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart | PatchPart | AgentPart | RetryPart | CompactionPart;

Ok so Part is a union of a bunch of parts type. I looked at the first one TextPart to see what it contains and it had something of type “text” which is what I’m looking for decided to try that out.

export type TextPart = {
    id: string;
    sessionID: string;
    messageID: string;
    type: "text";
    text: string;
    synthetic?: boolean;
    ignored?: boolean;
    time?: {
        start: number;
        end?: number;
    };
    metadata?: {
        [key: string]: unknown;
    };
};

I’m going to spare you the details but I logged the output to the console and saw that it did contain my message at the last element of the array of Part so I get the last element, tried to pull the text but when I had an error in my editor which is expected. Like we saw earlier, Part is actually a union of a bunch of thing, so we have to narrow it down for the compiler.

Once that’s done, the rest if pretty self explanatory, we just see if it contains any soft language and if it does, we create a notification

"chat.message": async (_, output) => {
  const lastPart = output.parts.at(-1);
  if (lastPart && lastPart.type === "text") {
    const hasSoftLanguage = softPatterns.some((pattern) =>
      lastPart.text.includes(pattern),
    );

    if (hasSoftLanguage) {
      await client.tui.showToast({
        body: {
          message: `don't use soft language for better response`,
          variant: "info",
        },
      });
    }
  }
},

Once you learn about type definitions, how to read them and the power of go to definition, finding the information you need is a lot easier. We didn’t have to use documentation really to learn about what each type contains could just look at it directly which is why we say TypeScript documents itself!