Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Writing and reviewing TypeScript — the type system and structural typing, generics, narrowing and inference, conditional/mapped/utility types, strictness configuration (tsconfig, the strict family), module resolution and project references, typing third-party/Node APIs, and declaration files (.d.ts).
SkillScore100/100
Test results 100
3/3 safety checks passed
73%100%of test cases passed +27 pts
Passed 22 of 22 test cases with the skill, and 16 of 22 without it.
6 fixed16 passed either way0 still failing0 broke
One test taskWe are preparing our codebase for TypeScript 6.0/7.0 upgrade and checking for obsolete compiler options. Which bundling-related option that historically concatenated JS output into a single file was completely removed in TypeScript 6.0?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add toddkasper-typescript
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install toddkasper-typescriptThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/toddkasper-typescript/SKILL.md https://app.decimal.ai/s/toddkasper-typescript/SKILL.md.claude/skills/toddkasper-typescript/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add toddkasper-typescriptimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add toddkasper-typescriptfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add toddkasper-typescriptimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add toddkasper-typescriptimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add toddkasper-typescriptfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add toddkasper-typescriptfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add toddkasper-typescriptimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add toddkasper-typescriptfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 1 other file is not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add toddkasper-typescriptfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add toddkasper-typescriptimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 1 other file is not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add toddkasper-typescriptimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 1 other file is not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add toddkasper-typescriptfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("toddkasper-typescript")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/toddkasper-typescript/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/toddkasper-typescript/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 1 other file is not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add toddkasper-typescriptNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/toddkasper-typescript/SKILL.md https://app.decimal.ai/s/toddkasper-typescript/SKILL.md.agents/skills/toddkasper-typescript/SKILL.mdThis gets SKILL.md only. The skill's 1 other file is not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 1 other file is not included.
Raw files: /s/toddkasper-typescript/SKILL.md · /s/toddkasper-typescript.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/toddkasper-typescript/SKILL.mdcurl -fsSL https://app.decimal.ai/s/toddkasper-typescript.jsonThis gets SKILL.md only. The skill's 1 other file is not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
TypeScript is a statically typed superset of JavaScript maintained by Microsoft. The goal of this
skill is the working competence a strong TypeScript engineer applies: using the type system to make
illegal states unrepresentable, without fighting the compiler or hiding behind any. There is no
official TypeScript certification; competence is anchored to the TypeScript Handbook and official
release notes.
Scope of this skill: the TypeScript type system and compiler toolchain — type syntax,
inference, narrowing, generics, configuration, and declaration files. Framework usage (React JSX,
Next.js server components, Node.js APIs) is deferred to the sibling skills (react, nextjs,
nodejs).
Load this skill when… designing or reviewing type models (discriminated unions, generics, mapped types); configuring
tsconfig.json(strictness, module resolution, project references); authoring or fixing.d.tsdeclaration files; migrating a JavaScript codebase to TypeScript. Not this skill: React component and hook typing → seereact; Node.js API typing in the context of building a service → seenodejs; Next.js server-component typing → seenextjs; React Native/Expo typing → seereact-native.
Study resources and version history live in references/study-resources.md — load that file when planning a learning path or verifying a version-specific fact.
Verify steps assume nothing about your tooling — use your project's own scripts and the language toolchain (
tsc,node, the test runner, the package manager), in that order of preference.
npm ls typescript) and compare against the release notes. [volatile — verify live] marks apply to: strict default (was false before TS 6.0); module/target defaults (changed in TS 6.0); types default (changed from auto-load all to [] in TS 6.0); --moduleResolution node deprecation timeline (deprecated 6.0, removal in 7.0 — [volatile — verify live]); const type parameters (TS 5.0+ only); TypeScript 7.0 Go-native compiler timeline and option removal (Beta shipped 2026-04-21; stable expected ~June 2026 — [volatile — verify live]).strict: false overrides across a large legacy codebase (compile errors may cascade); dependency major-version bumps; production deploys after tsconfig changes.[volatile — verify live] or [opinion — house style].[volatile — verify live]TypeScript 6.0 shipped March 2026 as the last release compiled by TypeScript itself. It changed nine defaults at once. Know these before touching a tsconfig in 2026.
| Option | Old default | New default (6.0) |
|---|---|---|
strict | false | true |
module | commonjs | esnext |
target | es5 | es2025 |
rootDir | inferred | . (tsconfig directory) |
types | ["*"] (all @types) | [] (none) |
noUncheckedSideEffectImports | false | true |
libReplacement | true | false |
Removed entirely in 6.0: --outFile, --module amd/umd/systemjs/none,
--moduleResolution classic, legacy module keyword (use namespace),
import assertions with assert (use with).
Deprecated in 6.0 (will be removed in 7.0): target: es5, --downlevelIteration,
--baseUrl, --moduleResolution node (node10), --esModuleInterop false,
--allowSyntheticDefaultImports false. Still usable in 6.0 via "ignoreDeprecations": "6.0";
must be cleaned up before adopting 7.0.
types: [] default is a stealth breaking change: any project that relied on auto-loaded
@types/node, @types/jest, etc., must now list them explicitly:
{ "compilerOptions": { "types": ["node", "jest"] } }
TypeScript 7.0 (Go-native compiler) [volatile — verify live] Beta shipped 2026-04-21; stable
expected ~June 2026 (RC a few weeks prior). Will remove everything deprecated in 6.0 and is
expected to deliver ~10× build speedups. All 6.0-deprecated options must be cleaned up before
adopting 7.0.
TypeScript uses structural (duck) typing — a type is compatible if its shape matches, regardless of nominal origin. Consequence: two interfaces with identical properties are interchangeable even if declared in different files or by different authors. This is intentional and correct; do not work around it with phantom properties unless you genuinely need nominal distinctions.
Literal types and widening: const x = "blue" infers type "blue"; let x = "blue" widens
to string. Add as const to freeze an object's values to their literal types:
const DIRECTIONS = ["north", "south", "east", "west"] as const;
type Direction = typeof DIRECTIONS[number]; // "north" | "south" | "east" | "west"
Template literal types compose string unions at the type level:
type EventName = `on${Capitalize<"click" | "focus">}`; // "onClick" | "onFocus"
TypeScript's control-flow analysis narrows union types as code branches. The narrowing tools, in order of preference:
| Technique | When to use |
|---|---|
typeof x === "string" | Primitives |
x instanceof Date | Class instances |
"swim" in x | Structural property presence |
Discriminated union (kind field) | Modeling mutually exclusive states |
Type predicate (x is Fish) | Custom guard, isolates runtime check |
Assertion function (asserts x is T) | Throws on failure, narrows after call |
never exhaustiveness check | Compile-time guarantee all union arms are handled |
Discriminated unions are the primary modeling tool for state machines and variant data. Use a
shared literal-typed kind (or type, tag) field; the compiler then narrows each branch
automatically in switch/if.
Exhaustiveness pattern — assign to never in the default branch so adding a new union member
causes a compile error in every switch that forgets to handle it:
function assertNever(x: never): never {
throw new Error("Unhandled case: " + x);
}
satisfies vs as: use satisfies to validate an object against a type while preserving the
most-specific inferred type (e.g., config objects, discriminated union literals). Use as only
when you genuinely know more than the compiler (e.g., DOM refs, casting from an untyped API
response after manual validation). as bypasses type checking entirely; satisfies does not.
Generics are parameterized types. Keep them simple: one parameter with a clear constraint is almost always better than three unconstrained parameters.
Constraints with extends: <T extends { length: number }> means "T must have at least
length." Constraints make generics useful without over-broadening.
const type parameters (TS 5.0+): <const T> infers literal types without requiring callers
to write as const:
function first<const T extends readonly unknown[]>(arr: T): T[0] { return arr[0]; }
When generics hurt: a generic function where every call site has T = the same concrete type is not gaining anything. Generics earn their complexity only when the relationship between input and output types (or between two parameters) genuinely varies per callsite.
T extends U ? X : Y — branch at the type level. Distributive by default over union T:
type Unwrap<T> = T extends Promise<infer U> ? U : T.
infer extracts a type from a pattern inside extends: use it to pull out return types,
element types, or parameter types. The built-in utility types ReturnType<F>,
Parameters<F>, Awaited<T> are implemented this way.
Avoid deeply nested conditional chains. More than two levels of nesting is a maintenance problem; consider a lookup table (mapped type over a union) instead.
Transform every key in a type:
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };
Key modifiers: readonly, ? (optional), -readonly (remove readonly), -? (remove optional).
Remap keys with as: { [K in keyof T as Capitalize<string & K>]: T[K] }.
| Utility | What it does |
|---|---|
Partial<T> | All props optional |
Required<T> | All props required |
Readonly<T> | All props readonly |
Pick<T, K> | Keep only keys K |
Omit<T, K> | Drop keys K |
Record<K, V> | Object with keys K and values V |
Exclude<T, U> | Union T minus U |
Extract<T, U> | Union T intersected with U |
NonNullable<T> | Remove null/undefined |
ReturnType<F> | Return type of function F |
Parameters<F> | Parameter tuple of function F |
Awaited<T> | Unwrap Promise (recursive) |
"strict": true (now the 6.0 default) enables nine sub-flags:
| Sub-flag | What it catches |
|---|---|
noImplicitAny | Variables/params inferred as any |
strictNullChecks | null/undefined not assignable to non-nullable types |
strictFunctionTypes | Contravariant param checking for function types |
strictBindCallApply | Correct argument types for .call/.bind/.apply |
strictPropertyInitialization | Class props must be assigned in constructor |
noImplicitThis | this typed as any in functions |
alwaysStrict | Emits "use strict" |
useUnknownInCatchVariables | Catch variable typed unknown not any |
strictBuiltinIteratorReturn | Built-in iterator TReturn is undefined not any (TS 5.6+) |
Opt-in beyond strict (not yet defaults, but strongly recommended):
| Flag | Why |
|---|---|
noUncheckedIndexedAccess | arr[i] returns T | undefined, forces index bounds awareness |
exactOptionalPropertyTypes | { x?: T } disallows explicit undefined assignment; cleaner option semantics |
noImplicitOverride | Must write override keyword when overriding a base method |
noUnusedLocals + noUnusedParameters | Dead code detection at compile time |
noFallthroughCasesInSwitch | Catches missing break |
noImplicitReturns | All code paths must return a value |
Gradual adoption: on a legacy JS→TS migration, set strict: false and enable flags one at a
time, starting with strictNullChecks (highest value/effort ratio). Never leave strict: false
on new code.
| Setting | Use when |
|---|---|
"module": "nodenext" + "moduleResolution": "nodenext" | Node.js packages with ESM or dual CJS/ESM |
"module": "esnext" + "moduleResolution": "bundler" | Bundler-based apps (Vite, webpack, esbuild) |
"module": "esnext" + "moduleResolution": "nodenext" | Not valid — do not mix |
--moduleResolution node (node10) is deprecated in 6.0; migrate to nodenext or bundler.
"moduleResolution": "bundler" is the right choice for any project that goes through a bundler;
it does not enforce the .js extension requirement that nodenext requires.
--module node20 (introduced TS 5.9) [volatile — verify live]: stable, non-floating alias for Node 20 behavior with
--target es2023 implied. Prefer over nodenext when pinning to Node 20 specifically.
Large monorepos should use project references ("composite": true, "references": [...]) to
enable incremental type checking per package. Requirements: "composite": true implies
"declaration": true. Add "declarationMap": true to enable jump-to-source across package
boundaries. Use tsc --build (or tsc -b) to build the reference graph.
@types packagesAs of TypeScript 6.0, "types": [] is the default — @types packages are not auto-loaded.
List every needed @types package explicitly in tsconfig.json under "types".
Resolution: TypeScript looks for @types/<package> automatically when you import "package". If
no @types package exists and the library ships no .d.ts, you must author one or use
declare module "package-name" as a stub (document this as technical debt).
DefinitelyTyped (github.com/DefinitelyTyped/DefinitelyTyped) hosts community-maintained
declarations for packages that don't ship their own types. Library authors writing in TypeScript
should ship .d.ts files directly (via "declaration": true) rather than relying on
DefinitelyTyped.
.d.ts)Write .d.ts files when:
Rules for .d.ts files:
export declare (not export) in module declaration files.declare global { ... } inside a module file.declare module "library" { ... } in a .d.ts file.interface for extensible shapes (consumers can merge), type for unions/tuples/aliases.any and type assertions are justifiedany legitimate uses (very short list):
any explicitly, track with // TODO: type this.JSON.stringify).any red flags:
(x as any).someMethod() to silence a compile error that would catch a real bug.as unknown as TargetType double-cast — almost always a type model problem.any from a public function's signature.For data crossing a runtime boundary (API responses, JSON.parse, environment variables), use a
runtime validator (Zod, Valibot, etc.) and let the validator produce the typed output — do not
cast raw parsed data with as.
JSON.parse, fetch().then(r => r.json()), req.body, process.env, or a third-party callback is unknown at the boundary..parse() to validate; on failure it throws a ZodError with field-level diagnostics. → gate: call .parse() with a deliberately malformed payload; confirm it throws ZodError, not a silent undefined.type MyPayload = z.infer<typeof MyPayloadSchema>. Do not write the type separately and then cast — the schema is the single source of truth..d.ts for a plain-JS library, load the library in a Node.js REPL and inspect Object.keys(require('the-lib')) and the prototype chain before declaring anything. → gate: node -e "const lib = require('the-lib'); console.log(typeof lib.method)" — only declare methods that return 'function'..d.ts alongside the JS output by setting "declaration": true and "declarationMap": true in tsconfig.json. → gate: tsc --build produces .d.ts and .d.ts.map files in dist/; consumers can jump-to-source across the package boundary.npm ls typescript). If on TS 6.0+, strict: true is already the default — confirm the project's tsconfig.json is not explicitly setting "strict": false. → gate: tsc --showConfig | grep strict reflects the actual effective value.strict: false is set and the codebase is large, enable sub-flags one at a time in order of value/effort ratio: strictNullChecks first (highest yield — catches most real bugs), then noImplicitAny, then the rest of the strict family.tsc --noEmit 2>&1 | wc -l to count new errors. Fix or explicitly type-assert with a // TODO: type this comment — never add // @ts-ignore without a ticket. → gate: error count decreases monotonically across commits; no // @ts-ignore without a linked tracking note.noUncheckedIndexedAccess and exactOptionalPropertyTypes after the strict family is clean — these are the highest-value non-strict additions. → gate: tsc --noEmit exits 0 with both flags enabled."noUnusedLocals": true and "noUnusedParameters": true in a CI-only tsconfig.ci.json that extends the base. Run it in CI to catch dead code before review without blocking local development. → gate: CI tsc -p tsconfig.ci.json --noEmit exits 0.kind, type, or tag) on every variant: { kind: 'circle'; radius: number }, { kind: 'rect'; w: number; h: number }. The discriminant must be a string literal, not a general string. → gate: tsc narrows correctly in a switch (shape.kind) block without type assertions.type Shape = Circle | Rect | Triangle.default branch, assign to never: const _exhaustive: never = shape. → gate: add a new union member (Square) without adding a case — tsc reports "Type 'Square' is not assignable to type 'never'" immediately.throw new Error('Unhandled shape: ' + (shape as any).kind). This satisfies noImplicitReturns and provides a runtime safety net for values that enter the union after a bad cast.satisfies (not as) when constructing union members to validate the literal shape while preserving the inferred specific type. → gate: attempting const s = { kind: 'circle' } satisfies Shape with a missing radius field produces a compile error, not a runtime one.Scenario 1 — A .d.ts declaration file promises a method that the underlying JS library does not export
Situation: A developer hand-writes a
.d.tsfile for an untyped legacy charting library. They add achart.destroy()method to the declaration because they saw it mentioned in an old README. After shipping, users callchart.destroy()and get a runtimeTypeError: chart.destroy is not a functionon the current version of the library.
Competent move: Audit the actual JS runtime export before declaring it. Load the library in a Node.js REPL and inspect its prototype, or read its source, before adding anything to the
.d.ts. The declaration file must faithfully describe what the JS actually does — phantom declarations give TypeScript false confidence and shift the error from compile time to runtime.
Tempting-but-wrong: Adding the declaration and leaving a
// TODO: verify this method existscomment, deferring the audit to later. Until the comment is resolved, every caller that useschart.destroy()compiles cleanly but crashes at runtime. "Works in TypeScript" gives no runtime guarantee when the declaration is a lie.
Verify: In a test file, import the library and call the declared method in isolation. Run
node test.js(nottsc) — the runtime will immediately throw if the method doesn't exist. Only after the runtime test passes should the declaration be considered correct.
Scenario 2 — Structural typing allows an OrderId to be passed where a UserId is expected
Situation: A codebase uses
type UserId = stringandtype OrderId = string. Both are aliases forstring. A functiongetOrder(userId: UserId, orderId: OrderId)is called in one place with the arguments reversed —getOrder(orderId, userId). TypeScript emits no error and the bug reaches production.
Competent move: Use branded (nominal) types for distinct ID domains. A minimal brand:
type UserId = string & { readonly __brand: 'UserId' }. Assign them via constructor functions:function toUserId(s: string): UserId { return s as UserId }. NowUserIdandOrderIdare structurally distinct and the compiler catches transposed arguments.
Tempting-but-wrong: Adding a code comment like
// first arg must be userIdand relying on review. Comments do not survive refactoring. A structural type alias (type UserId = string) provides no compiler protection whatsoever — both types are assignment-compatible with each other and withstring.
Verify: After branding, swap the arguments intentionally and run
tsc. The compiler should report "Argument of type 'OrderId' is not assignable to parameter of type 'UserId'." This is the compile-time guarantee the plain type alias could not provide.
Scenario 3 — Overly broad generic constraint lets any object through instead of enforcing a minimum shape
Situation: A generic utility
function pluck<T, K extends keyof T>(obj: T, key: K): T[K]is being used correctly. A developer then writes a new utilityfunction merge<T>(a: T, b: T): Tfor merging config objects. Code review flags theTconstraint as too broad — the function is called with non-plain-object values like arrays and class instances.
Competent move: Add a constraint that matches the intended usage:
function merge<T extends Record<string, unknown>>(a: T, b: T): T. This narrowsTto plain object types and prevents accidental calls with arrays, primitives, or class instances, all of which would compile without the constraint. The constraint communicates intent and catches misuse at the callsite.
Tempting-but-wrong: Removing generics entirely and typing the parameters as
object.objectexcludes primitives but is too broad — it includes arrays and class instances — and the return type loses the specific shape ofT, forcing callers to cast.
Verify: Attempt to call
merge([1, 2], [3, 4])with and without theRecord<string, unknown>constraint. Without it, the call compiles silently. With it, TypeScript reports "Argument of type 'number[]' is not assignable to parameter of type 'Record<string, unknown>'."
Scenario 4 — Runtime boundary: unknown API response cast with as instead of a runtime validator
Situation: A service receives a webhook payload, parses it with
JSON.parse, and immediately casts:const event = JSON.parse(body) as WebhookPayload. The type checks pass. In production, a vendor changes the payload schema and omits a required field — the code silently proceeds withundefinedwhere a string is expected, causing corrupt database writes.
Competent move: Use a runtime validator (Zod, Valibot, or similar) to parse and validate the payload at the boundary:
const event = WebhookPayloadSchema.parse(JSON.parse(body)). The validator throws on schema mismatch, turning a silent corruption into an explicit 400/500 error. The TypeScript type ofeventis then inferred from the schema — noascast needed.
Tempting-but-wrong: Adding defensive
if (event.field !== undefined)checks throughout the business logic downstream. This scatters validation across the codebase and is easy to miss for new fields. Validating at the single entry point (the boundary) catches all failures in one place regardless of how many fields the payload has.
Verify: Define a Zod schema that matches the expected payload. Call
.parse()with a deliberately malformed payload (missing a required field). Confirm the validator throws aZodErrorwith a clear field-level message. Then confirm the correctly shaped payload parses cleanly and the inferred type matches.
Scenario 5 — unknown vs any in an error-handling utility at an API edge
Situation: A shared
handleError(err: any)utility function is used throughout the codebase to log and format errors. A junior engineer proposes changingerr: anytoerr: unknownbut a teammate objects that doing so will break every call site whereerr.messageis accessed directly.
Competent move: Change the parameter to
unknownand add a type guard at the top of the function:const message = err instanceof Error ? err.message : String(err). This is exactly the right design for a shared error handler — errors from any source (rejected Promises, thrown strings, legacy code) can have any shape.unknownforces the narrowing to happen inside the utility, once, rather than spreading unchecked.messageaccess across every call site.
Tempting-but-wrong: Keeping
err: anyto avoid touching existing code.anypropagates upward — every downstream expression derived fromerralso becomesany, silently disabling type checking on anything the error touches. A singleunknown+ guard in the utility is a smaller and safer change thananyleft in place indefinitely.
Verify: With
err: unknownin place, attempt to accesserr.messagedirectly (without a guard) inside the utility.tscshould report "Property 'message' does not exist on type 'unknown'." After adding theinstanceof Errorguard, the access compiles and the check is explicit.
kind literal field) for mutually exclusive
states — the compiler narrows automatically.satisfies to validate config/literal objects against a type while keeping the
inferred literal shape; prefer it over as for object initialization.as to silence a type error — fix the type model. Reserve as for genuine
type-narrowing the compiler can't see (post-validation casts, DOM refs).any without a comment explaining why; prefer unknown and narrow it.strict: true on all new code (it is now the 6.0 default). Layer
noUncheckedIndexedAccess and exactOptionalPropertyTypes on top for stricter semantics.noUncheckedIndexedAccess by casting — add bounds checks or use .at() with a
default.switch over a discriminated union by assigning the default to
never; this turns a missing case into a compile error.enum in new code — prefer as const objects + a derived union type
(smaller bundle, works with erasableSyntaxOnly, no runtime artifact).const type parameters (<const T>) to infer literal types from call-site values
without requiring as const at the callsite (TS 5.0+).--module nodenext (or node20) + "moduleResolution": "nodenext" for Node
packages; use --module esnext + "moduleResolution": "bundler" for bundled apps."types": ["node"] (or whichever @types you need) in tsconfig — the
6.0 default is "types": [], so nothing is auto-loaded.--baseUrl, --moduleResolution node,
target: es5, --downlevelIteration) in new projects; they will be removed in TypeScript 7.0.
(--outFile and --moduleResolution classic were removed outright in 6.0 — do not use at all.)"declarationMap": true in library packages so consumers can jump-to-source.JSON.parse(...) with as MyType — use a runtime validator (Zod/Valibot) and
let the validator produce the typed output..d.ts declarations honest: declare only what actually exists at runtime.noUnusedLocals: true and noUnusedParameters: true in CI configs to catch dead
code before review.any in a public API signature.as unknown as T double-cast (structural lie — find the real type mismatch).as const + derived union).arr[i] used without a bounds check, especially when
noUncheckedIndexedAccess is not enabled.foo?: string used everywhere as foo?: string | undefined — with
exactOptionalPropertyTypes these differ.catch (e) block casting e as Error without checking — TS 6.0 makes e: unknown the
default; narrow it with instanceof Error..d.ts file declaring methods or properties the underlying JS library does not actually export.tsconfig.json that still has "moduleResolution": "node" (node10) or --baseUrl — both
deprecated in 6.0, removed in 7.0.JSON.parse(rawInput) as SomeType without a runtime validation step.Using this skill and hit a wall? If you find a claim contradicted by the live system or official docs, a missing rule that cost you a wrong attempt, or a decision this skill gave no criteria for — append an entry in the moment to .skill-feedback/typescript.md at the project root (create it if absent):
date | skill last-reviewed | claim or gap | what you observed instead | evidence (error text / doc URL / query output) | suggested fix
These are harvested back into the skill via the learning loop. When the live system and this file disagree, trust the live system.
target: es5 and --downlevelIteration moved from "Removed in 6.0" to "Deprecated in 6.0" (still usable via ignoreDeprecations; removed in 7.0); (3) strict sub-flag count corrected 8→9, added strictBuiltinIteratorReturn (TS 5.6); (4) libReplacement default true→false added to 6.0 defaults table; Operational Rules updated to match. Four held-out eval probes added (situations 13–16). last-reviewed set to 2026-06-10.[volatile — verify live] marks, executable workflows, tool-agnostic verify steps, and the feedback protocol above. last-reviewed set to 2026-06-09.Independent educational content to upskill AI agents. TypeScript is a trademark of Microsoft. Not affiliated with or endorsed by Microsoft. Guidance only — verify against official documentation at typescriptlang.org.
Automatically, when a task matches. Use when adding or reviewing types in any TS codebase (Node, React, Lambdas). Framework-specific concerns live in the react/nodejs/nextjs/react-native skills. Competence skill anchored on the offici
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.