Documentation Strategy
LLMs decide which function to call by reading its description. Good descriptions are not optional — they are the prompt. This page covers three practical patterns that make the LLM behave better without changing any TypeScript types:
- Description comments — JSDoc on the method and on the parameter type
- Namespace inheritance — description on the parent type cascades into children
- Hiding methods — exclude internal helpers from the schema
Plus two carry-overs from the schema layer for reference:
- Specialization —
@title,@deprecated,@hidden,@internal - Custom
x-…fields — vendor extensions in JSON Schema
Description comments
The LLM reads:
- The method JSDoc comment as the function’s
description - Property JSDoc comments as each property’s
description - The
@titlecomment tag (if present) as the property’stitle
class BbsArticleController {
/**
* Create a new article.
*
* Writes a new article and archives it into the DB.
*/
async create(props: {
/**
* Information of the article to create.
*/
input: IBbsArticle.ICreate;
}): Promise<IBbsArticle> { /* … */ }
}Console output of what the LLM sees for create:
Create a new article.
Writes a new article and archives it into the DB.The more the description tells the LLM about when to call the function (not just what it returns), the more reliably it picks the right one.
TypeScript Source
import typia, { ILlmApplication, ILlmFunction, tags } from "typia";
const app: ILlmApplication = typia.llm.application<BbsArticleController>();
const func: ILlmFunction | undefined = app.functions.find(
(func) => func.name === "create",
);
console.log(func?.description);
interface BbsArticleController {
/**
* Create a new article.
*
* Writes a new article and archives it into the DB.
*
* @param props Properties of create function
* @returns Newly created article
*/
create(props: {
/** Information of the article to create */
input: IBbsArticle.ICreate;
}): Promise<IBbsArticle>;
/**
* Update an article.
*
* Updates an article with new content.
*
* @param props Properties of update function
* @param input New content to update
*/
update(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
/** New content to update. */
input: IBbsArticle.IUpdate;
}): Promise<void>;
/**
* Erase an article.
*
* Erases an article from the DB.
*
* @param props Properties of erase function
*/
erase(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
}): Promise<void>;
}
/**
* Article entity.
*
* `IBbsArticle` is an entity representing an article in the BBS (Bulletin Board
* System).
*/
interface IBbsArticle extends IBbsArticle.ICreate {
/** Primary Key. */
id: string & tags.Format<"uuid">;
/** Creation time of the article. */
created_at: string & tags.Format<"date-time">;
/** Last updated time of the article. */
updated_at: string & tags.Format<"date-time">;
}
namespace IBbsArticle {
/** Information of the article to create. */
export interface ICreate {
/**
* Title of the article.
*
* Representative title of the article.
*/
title: string;
/**
* Content body.
*
* Content body of the article writtn in the markdown format.
*/
body: string;
/**
* Thumbnail image URI.
*
* Thumbnail image URI which can represent the article.
*
* If configured as `null`, it means that no thumbnail image in the article.
*/
thumbnail:
| null
| (string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">);
}
/**
* Information of the article to update.
*
* Only the filled properties will be updated.
*/
export type IUpdate = Partial<ICreate>;
}description vs title
ILlmFunction has one description. ILlmSchema has both description and title. Use @title to add a short heading on a property without it being repeated in the description:
import { tags } from "typia";
export interface IMember {
/**
* Primary Key.
*
* This description would be the description of LLM schema.
*/
id: string;
/**
* Below "Age of the member" would be the title of LLM schema.
*
* @title Age of the member
*/
age: number &
tags.Type<"uint32"> &
tags.ExclusiveMinimum<19> &
tags.Maximum<100>;
/**
* The description property also can be filled by the comment tag
* `@description`. Instead, be careful about the indentation.
*
* @title Email address of the member
*/
email: string & tags.Format<"email">;
}You can also fill description with the @description comment tag, but watch the indentation — every continuation line of a @description block must keep its leading whitespace consistent, otherwise only the first line gets captured and the rest is silently dropped. If a generated schema’s description looks oddly truncated, this is almost always the cause. Plain JSDoc (no @description tag) doesn’t have this problem.
Namespace inheritance
CRUD-style controllers tend to have parent types (IBbsArticle) and child variants (IBbsArticle.ICreate, IBbsArticle.IUpdate). Repeating the same long description on every variant is tedious. typia copies the parent’s description into the children automatically:
TypeScript Source
import typia, { ILlmApplication, ILlmFunction, tags } from "typia";
const app: ILlmApplication = typia.llm.application<BbsArticleController>();
const func: ILlmFunction | undefined = app.functions.find(
(func) => func.name === "create",
);
console.log(func?.parameters.properties.input?.description);
/**
* Article entity.
*
* `IBbsArticle` is an entity representing an article in the BBS (Bulletin Board
* System).
*/
interface IBbsArticle extends IBbsArticle.ICreate {
/** Primary Key. */
id: string & tags.Format<"uuid">;
/** Creation time of the article. */
created_at: string & tags.Format<"date-time">;
/** Last updated time of the article. */
updated_at: string & tags.Format<"date-time">;
}
namespace IBbsArticle {
/** Information of the article to create. */
export interface ICreate {
/**
* Title of the article.
*
* Representative title of the article.
*/
title: string;
/**
* Content body.
*
* Content body of the article writtn in the markdown format.
*/
body: string;
/**
* Thumbnail image URI.
*
* Thumbnail image URI which can represent the article.
*
* If configured as `null`, it means that no thumbnail image in the article.
*/
thumbnail:
| null
| (string & tags.Format<"uri"> & tags.ContentMediaType<"image/*">);
}
/**
* Information of the article to update.
*
* Only the filled properties will be updated.
*/
export type IUpdate = Partial<ICreate>;
}
interface BbsArticleController {
/**
* Create a new article.
*
* Writes a new article and archives it into the DB.
*
* @param props Properties of create function
* @returns Newly created article
*/
create(props: {
/** Information of the article to create */
input: IBbsArticle.ICreate;
}): Promise<IBbsArticle>;
/**
* Update an article.
*
* Updates an article with new content.
*
* @param props Properties of update function
* @param input New content to update
*/
update(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
/** New content to update. */
input: IBbsArticle.IUpdate;
}): Promise<void>;
/**
* Erase an article.
*
* Erases an article from the DB.
*
* @param props Properties of erase function
*/
erase(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
}): Promise<void>;
}Write the parent’s description once, give each child a short variant-specific note, and the LLM sees both — the variant note and the inherited parent context.
Hiding methods
Sometimes you want a method on the class but not on the LLM’s tool list — an internal helper, a sensitive admin action, anything the LLM shouldn’t be calling. Use one of these JSDoc tags on the method:
| Tag | Means |
|---|---|
@hidden | Excluded from the LLM application |
@internal | Excluded (TypeScript convention for “not part of the public API”) |
@human | Excluded (typia convention for “only humans, not LLMs, should call this”) |
TypeScript Source
import typia, { ILlmApplication, tags } from "typia";
import { IBbsArticle } from "./IBbsArticle";
const app: ILlmApplication = typia.llm.application<BbsArticleController>();
console.log(app);
interface BbsArticleController {
/**
* Create a new article.
*
* Writes a new article and archives it into the DB.
*
* @param props Properties of create function
* @returns Newly created article
*/
create(props: {
/** Information of the article to create */
input: IBbsArticle.ICreate;
}): Promise<IBbsArticle>;
/**
* Read an article.
*
* Reads an article from the DB.
*
* @ignore
* @param props Properties of read function
* @returns The article
*/
at(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
}): Promise<IBbsArticle>;
/**
* Update an article.
*
* Updates an article with new content.
*
* @param props Properties of update function
* @param input New content to update
* @internal
*/
update(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
/** New content to update. */
input: IBbsArticle.IUpdate;
}): Promise<void>;
/**
* Erase an article.
*
* Erases an article from the DB.
*
* @param props Properties of erase function
* @human
*/
erase(props: {
/** Target article's {@link IBbsArticle.id}. */
id: string & tags.Format<"uuid">;
}): Promise<void>;
}Hidden methods don’t appear in ILlmApplication.functions, so the LLM never sees them. They still exist on the class instance and you can call them normally from your own code.
Specialization
The same comment tags work for properties on the parameter and return types:
| Tag | Effect |
|---|---|
@title Something | Short title shown alongside description |
@deprecated | Marks the property as deprecated in the schema |
@hidden / @internal | Excludes the property from the emitted schema |
TypeScript Source
import typia, { ILlmSchema, tags } from "typia";
export const $defs: Record<string, ILlmSchema> = {};
export const schema: ILlmSchema = typia.llm.schema<Special>($defs);
interface Special {
/**
* Deprecated tags are just used for marking.
*
* @deprecated
* @title Unsigned integer
*/
type: number & tags.Type<"int32">;
/**
* Internal tagged property never be shown in JSON schema.
*
* It even doesn't be shown in other `typia` functions like `assert<T>()`.
*
* @internal
*/
internal: number[];
/**
* Hidden tagged property never be shown in JSON schema.
*
* However, it would be shown in other `typia` functions like
* `stringify<T>()`.
*
* @ignore
*/
hidden: boolean;
/**
* You can limit the range of number.
*
* @exclusiveMinimum 19
* @maximum 100
*/
number?: number;
/**
* You can limit the length of string.
*
* Also, multiple range conditions are also possible.
*/
string: string &
(
| (tags.MinLength<3> & tags.MaxLength<24>)
| (tags.MinLength<40> & tags.MaxLength<100>)
);
/**
* You can limit the pattern of string.
*
* @pattern ^[a-z]+$
*/
pattern: string;
/**
* You can limit the format of string.
*
* @format date-time
*/
format: string | null;
/** In the Array case, possible to restrict its elements. */
array: Array<string & tags.Format<"uuid">> & tags.MinItems<3>;
}Also see type tags — the same Format, Minimum, MaxLength, etc. that drive runtime validation also flow into the emitted JSON Schema as format, minimum, maxLength.
Customization
JSON Schema reserves the x- prefix for non-standard extensions. To add x-… keys, use tags.TagBase.schema or the tags.JsonSchemaPlugin shortcut:
TypeScript Source
import typia, { ILlmSchema, tags } from "typia";
export const $defs: Record<string, ILlmSchema> = {};
export const schema: ILlmSchema = typia.llm.schema<IAccount>($defs);
type Monetary<Value extends string> = tags.TagBase<{
target: "number";
kind: "monetary";
value: Value;
schema: {
"x-monetary": Value;
};
}>;
type Placeholder<Value extends string> = tags.JsonSchemaPlugin<{
"x-placeholder": Value;
}>;
interface IAccount {
code: string & Placeholder<"Write you account code please">;
balance: number & Monetary<"dollar">;
}Where to go next
- The class-based tool API →
typia.llm.application - Schema for one structured output →
structuredOutput·parameters - The validation feedback channel →
LlmJson - Constraint tags → Special Tags