
Lang Typescript
- 61 installs
- 16 repo stars
- Updated June 10, 2026
- ravnhq/ai-toolkit
Helps with ai & agent building tasks.
About
lang-typescript is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- lang-typescript
- AI & Agent Building
- AI-coding skill
Lang Typescript by the numbers
- 61 all-time installs (skills.sh)
- Ranked #6,381 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ravnhq/ai-toolkit --skill lang-typescriptAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 61 |
|---|---|
| repo stars | ★ 16 |
| Last updated | June 10, 2026 |
| Repository | ravnhq/ai-toolkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
Principles
- Enable strict mode — no implicit any, strict null checks
- Prefer discriminated unions over type assertions
- Use
unknownoverany— narrow with type guards
Rules
See rules index for detailed patterns.
Examples
Positive Trigger
User: "Replace unsafe any usage with discriminated unions in this module."
Expected behavior: Use lang-typescript guidance, follow its workflow, and return actionable output.
Non-Trigger
User: "Design REST route naming conventions for a new backend."
Expected behavior: Do not prioritize lang-typescript; choose a more relevant skill or proceed without it.
Troubleshooting
Skill Does Not Trigger
- Error: The skill is not selected when expected.
- Cause: Request wording does not clearly match the description trigger conditions.
- Solution: Rephrase with explicit domain/task keywords from the description and retry.
Guidance Conflicts With Another Skill
- Error: Instructions from multiple skills conflict in one task.
- Cause: Overlapping scope across loaded skills.
- Solution: State which skill is authoritative for the current step and apply that workflow first.
Output Is Too Generic
- Error: Result lacks concrete, actionable detail.
- Cause: Task input omitted context, constraints, or target format.
- Solution: Add specific constraints (environment, scope, format, success criteria) and rerun.
Workflow
1. Identify whether the request clearly matches lang-typescript scope and triggers. 2. Apply the skill rules and referenced guidance to produce a concrete result. 3. Validate output quality against constraints; if gaps remain, refine once with explicit assumptions.
Type Safety
- type-no-any — Never use
any— useunknown, generics, or proper types instead - type-no-assertions — Avoid type assertions (
as) — use type guards or discriminated unions
Async
- prefer-async-await — Use async/await over .then() chains; Promise.all for parallel work
Modules
- export-named-only — Always use named exports; maintain consistent import order
Always Use Named Exports
Use named exports exclusively. Never use default exports. Named exports enable better refactoring (renames propagate automatically), better IDE autocomplete (the editor knows what a module exports), and easier grep-ability (you can search for exact symbol names). Maintain consistent import ordering across all files.
Incorrect (default exports, unordered imports):
// Bad - default export
export default function UserProfile({ userId }: Props) {
// ...
}
// Bad - default can be imported under any name, causing inconsistency
import UserProfile from "./UserProfile";
import Profile from "./UserProfile"; // Same thing, different name
// Bad - unordered imports
import { usePosts } from "./use-posts";
import { z } from "zod";
import { Button } from "@/shared/components/ui/button";
import { useState } from "react";Correct (named exports, ordered imports):
// Good - named export
export function UserProfile({ userId }: Props) {
// ...
}
// Good - named import is consistent everywhere
import { UserProfile } from "./UserProfile";
// Good - imports ordered: external -> shared/internal -> local
import { useState } from "react";
import { z } from "zod";
import { db } from "@/db";
// Shared/internal packages
import { Button } from "@/shared/components/ui/button";
// Local/feature imports
import { usePosts } from "./use-posts";Guidelines:
- Always use named exports -- never
export default - Named exports enforce consistent naming across the codebase
- Import order: external packages, then shared/internal packages, then local imports -- separated by blank lines
- This applies to all file types: components, utilities, hooks, types, constants
Prefer async/await Over .then() Chains
Use async/await for asynchronous code. It reads top-to-bottom, handles errors naturally with try/catch, and avoids callback nesting. Use Promise.all() for independent parallel operations and Promise.allSettled() when you need results from all promises even if some fail. Never use raw callbacks when promises are available.
Incorrect (nested .then() chains):
// Bad - chained .then() is harder to read and debug
function loadUserDashboard(userId: string) {
return db.query.users.findFirst({ where: eq(users.id, userId) })
.then(user => {
return db.query.organizations.findFirst({ where: eq(orgs.id, user.orgId) })
.then(org => {
return db.query.posts.findMany({ where: eq(posts.orgId, org.id) })
.then(posts => {
return { user, org, posts };
});
});
})
.catch(error => {
console.error("Failed:", error);
throw error;
});
}Correct (async/await with try/catch, Promise.all for parallel):
// Good - sequential when operations depend on each other
async function getUser(userId: string) {
try {
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (!user) throw new NotFoundError("User not found");
const org = await db.query.organizations.findFirst({
where: eq(orgs.id, user.orgId),
});
return { user, org };
} catch (error) {
console.error("Failed to load user", { userId, error: error.message });
throw error;
}
}
// Good - parallel when operations are independent
async function loadDashboard(userId: string, orgId: string) {
const [user, org, posts] = await Promise.all([
db.query.users.findFirst({ where: eq(users.id, userId) }),
db.query.organizations.findFirst({ where: eq(orgs.id, orgId) }),
db.query.posts.findMany({ where: eq(posts.orgId, orgId) }),
]);
return { user, org, posts };
}
// Good - allSettled when you need results even if some fail
async function sendNotifications(userIds: string[]) {
const results = await Promise.allSettled(
userIds.map(id => notifyUser(id)),
);
const failures = results.filter(r => r.status === "rejected");
if (failures.length > 0) {
console.warn("Some notifications failed", { failureCount: failures.length });
}
}Guidelines:
- Always use async/await instead of
.then()chains - Use
Promise.all()when multiple independent operations can run in parallel - Use
Promise.allSettled()when you need all results regardless of individual failures - Never use raw callbacks (e.g.,
fs.readFile(path, callback)) when a promise-based API is available
Use Discriminated Unions for Type-Safe Variants
When you have a union type with multiple variants, use a discriminator field to enable exhaustive checking and eliminate the need for type assertions.
Incorrect (manual type narrowing with assertions):
// Union without discriminator - requires manual narrowing
type ApiResponse =
| { data: User[] }
| { error: string };
function handleResponse(response: ApiResponse) {
// Unsafe - have to guess which variant
if ('data' in response) {
return response.data; // TypeScript can't verify this is safe
}
if ('error' in response) {
return response.error;
}
// No exhaustiveness checking - could add new variant and miss it here
}
// Redux actions without discriminator
type Action =
| { userId: string; name: string }
| { userId: string; error: string };
function reducer(state: State, action: Action) {
// Can't tell if this is success or error action!
if ('name' in action) {
return { ...state, user: { id: action.userId, name: action.name } };
}
// What if both 'name' and 'error' are present?
}Correct (discriminated union with exhaustive checking):
// Add 'type' discriminator field
type ApiResponse =
| { type: 'success'; data: User[] }
| { type: 'error'; error: string };
function handleResponse(response: ApiResponse) {
switch (response.type) {
case 'success':
return response.data; // TypeScript knows this is the success variant
case 'error':
return response.error; // TypeScript knows this is the error variant
// If you add a new variant, TypeScript forces you to handle it here
}
}
// Redux actions with discriminator
type Action =
| { type: 'USER_LOADED'; userId: string; name: string }
| { type: 'USER_ERROR'; userId: string; error: string }
| { type: 'USER_LOADING'; userId: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'USER_LOADED':
// TypeScript knows 'name' exists here
return { ...state, user: { id: action.userId, name: action.name } };
case 'USER_ERROR':
// TypeScript knows 'error' exists here
return { ...state, error: action.error };
case 'USER_LOADING':
return { ...state, loading: true };
// Compiler error if we forget to handle a variant!
}
}Result type pattern:
// Classic Result<T, E> pattern for fallible operations
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function loadUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findUnique({ where: { id } });
return { ok: true, value: user };
} catch (error) {
return { ok: false, error: error as Error };
}
}
// Usage - compiler forces you to check
const result = await loadUser('123');
if (result.ok) {
console.log(result.value.name); // TypeScript knows 'value' exists
} else {
console.error(result.error.message); // TypeScript knows 'error' exists
}Discriminator fields:
Common names for the discriminator field:
type- for tagged unions, actions, eventskind- for AST nodes, variantsstatus- for state machinesok/success- for Result types (boolean discriminator)
Why it matters:
- Exhaustiveness checking: TypeScript will error if you forget to handle a variant
- No type assertions needed: Discriminator narrows the type automatically
- Refactoring safety: Adding/removing variants causes compile errors at all usage sites
- Self-documenting: The discriminator field explicitly names each variant
- IDE support: Better autocomplete and go-to-definition for variant-specific fields
Never Use any
The any type disables TypeScript's protection. Use unknown and narrow, or fix the underlying type issue.
Incorrect:
function processData(data: any) {
return data.items.map((item: any) => item.name);
}
// Silently accepts anything, crashes at runtime
processData("not an object"); // Runtime error: Cannot read 'items' of stringCorrect:
interface DataWithItems {
items: Array<{ name: string }>;
}
function processData(data: DataWithItems) {
return data.items.map(item => item.name);
}
// Or with unknown + validation
function processUnknown(data: unknown) {
if (!isDataWithItems(data)) {
throw new Error('Invalid data format');
}
return data.items.map(item => item.name);
}Also avoid:
@ts-nocheck- never use@ts-ignore- only with explicit approval for exceptional cases- Type assertions (
as) - fix the underlying type instead
Why it matters: Every any is a potential runtime crash. TypeScript exists to catch errors at compile time - don't circumvent it.
Avoid Type Assertions
Type assertions (as) tell TypeScript "trust me" - but you might be wrong. Prefer proper typing or runtime validation.
Incorrect:
// Assuming API returns what you expect
const user = (await fetchUser()) as User;
user.email.toLowerCase(); // Crashes if email is null
// Forcing incompatible types
const config = rawConfig as AppConfig;Correct:
// Runtime validation with Zod
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});
const user = UserSchema.parse(await fetchUser());
// Now TypeScript AND runtime guarantee the shape
// Or use type guards
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'email' in data &&
typeof data.email === 'string'
);
}When assertions are acceptable:
- Test code where you control the mock data
- After a type guard in the same scope
- DOM element types after null checks
Why it matters: Assertions hide type mismatches that become runtime errors. Runtime validation ensures data matches expectations at system boundaries.