重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
typescript-best-practices by ofershap/typescript-best-practices
npx skills add https://github.com/ofershap/typescript-best-practices --skill typescript-best-practices在处理 TypeScript 代码时使用此技能。AI 代理经常生成过时的模式——使用 any 而不是 unknown,使用类型断言而不是 satisfies,使用可选字段而不是可辨识联合,以及缺少严格模式选项。此技能强制实施现代 TypeScript 5.x 模式。
错误(代理常这样做):
{
"compilerOptions": {
"strict": false,
"target": "ES2020"
}
}
正确:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"target": "ES2022"
}
}
原因: 严格模式能捕获整类错误。noUncheckedIndexedAccess 防止不安全的数组/对象访问。代理们常常为了"方便"而跳过这些设置。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
错误(代理常这样做):
const config = {
port: 3000,
host: "localhost",
} as Config;
config.port.toFixed(); // 即使 port 可能是字符串也不会报错
正确:
const config = {
port: 3000,
host: "localhost",
} satisfies Config;
config.port.toFixed(); // TypeScript 知道 port 是数字
原因: satisfies 验证类型而不扩大其范围。as 会使编译器静默,并可能隐藏错误。使用 satisfies 进行验证,仅在你确实比编译器知道更多信息时才使用 as。
错误(代理常这样做):
interface ApiResponse {
data?: User;
error?: string;
loading?: boolean;
}
正确:
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
原因: 可选字段允许不可能的状态(data 和 error 同时存在)。可辨识联合使每个状态都明确且可进行穷尽性检查。
错误(代理常这样做):
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
};
// 类型: { home: string; about: string; contact: string }
正确:
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
} as const;
// 类型: { readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }
原因: 没有 as const,TypeScript 会将字面量类型扩大为 string。有了它,你会得到精确的字面量类型和只读属性。
错误(代理常这样做):
function parseJson(text: string): any {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // 无错误 - 运行时崩溃
正确:
function parseJson(text: string): unknown {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
if (isUser(data)) {
data.name; // 安全 - 类型已收窄
}
原因: any 会禁用所有类型检查。unknown 强制你在使用前收窄类型,从而在编译时捕获错误。
错误(代理常这样做):
function getLocaleMessage(id: string): string { ... }
正确:
type Locale = 'en' | 'ja' | 'pt';
type MessageKey = 'welcome' | 'goodbye';
type LocaleMessageId = `${Locale}_${MessageKey}`;
function getLocaleMessage(id: LocaleMessageId): string { ... }
原因: 模板字面量类型从联合类型创建精确的字符串模式。编译器会在构建时捕获拼写错误和无效组合。
错误(代理常这样做):
function createLight<C extends string>(colors: C[], defaultColor?: C) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // 无错误 - purple 扩大了 C 的范围
正确:
function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // 错误 - 'purple' 不在 C 中
原因: NoInfer<T>(TypeScript 5.4+)防止参数影响类型推断,确保更严格的检查。
错误(代理常这样做):
function getUser(id: string): User { ... }
function getOrder(id: string): Order { ... }
const userId = getUserId();
getOrder(userId); // 无错误 - 但这是错误的!
正确:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
function getUser(id: UserId): User { ... }
function getOrder(id: OrderId): Order { ... }
const userId = getUserId();
getOrder(userId); // 错误 - UserId 不是 OrderId
原因: 标记类型防止意外地将一种 ID 类型传递到期望另一种 ID 类型的地方。标记仅在编译时存在——运行时成本为零。
错误(代理常这样做):
function handleStatus(status: "active" | "inactive" | "pending") {
switch (status) {
case "active":
return "Active";
case "inactive":
return "Inactive";
// 'pending' 会静默地落入默认情况
}
}
正确:
function handleStatus(status: "active" | "inactive" | "pending") {
switch (status) {
case "active":
return "Active";
case "inactive":
return "Inactive";
case "pending":
return "Pending";
default: {
const _exhaustive: never = status;
throw new Error(`Unhandled status: ${_exhaustive}`);
}
}
}
原因: never 检查确保处理了联合类型的每个成员。当添加新状态时,编译器会标记出缺失的 case。
错误(代理常这样做):
function processItem(item: unknown) {
const user = item as User;
console.log(user.name);
}
正确:
function isUser(item: unknown): item is User {
return typeof item === "object" && item !== null && "name" in item && "email" in item;
}
function processItem(item: unknown) {
if (isUser(item)) {
console.log(item.name); // 安全 - 已收窄为 User 类型
}
}
原因: 类型谓词(item is User)通过运行时检查安全地收窄类型。类型断言(as User)会绕过编译器,并可能隐藏错误。
错误(代理常这样做):
import { User, UserService } from "./user";
// User 仅用作类型,但仍会被包含在打包文件中
正确:
import type { User } from "./user";
import { UserService } from "./user";
原因: import type 在编译时会被擦除,从而减少打包体积。它也使意图更清晰——此导入仅用于类型。
错误(代理常这样做):
interface Config {
[key: string]: string;
}
正确:
type Config = Record<string, string>;
// 或者更好——对键使用特定的联合类型:
type Config = Record<"host" | "port" | "env", string>;
原因: Record<K, V> 比索引签名更具可读性和可组合性。在可能的情况下,对键使用联合类型以获得穷尽性检查。
错误(代理常这样做):
const file = openFile("data.txt");
try {
processFile(file);
} finally {
file.close();
}
正确:
using file = openFile("data.txt");
processFile(file);
// file.close() 通过 Symbol.dispose 自动调用
原因: using 关键字(TypeScript 5.2+)通过 Disposable 协议提供确定性的资源清理,类似于 Python 的 with 或 C# 的 using。
strict: true 和 noUncheckedIndexedAccess: truesatisfies 进行类型验证而不扩大类型范围type 或 kind 字段的可辨识联合进行状态建模as constimport typenever 默认情况的穷尽性 switch 来处理联合类型any —— 使用 unknown 并通过类型守卫收窄as 进行类型断言,除非你确实比编译器知道更多信息// @ts-ignore 或 // @ts-expect-error 而不附上解释原因的注释enum —— 改用 as const 对象或联合类型Function 类型——使用具体的函数签名每周安装数
59
代码仓库
GitHub 星标数
1
首次出现
2026年2月20日
安全审计
安装于
opencode58
cursor57
gemini-cli56
cline56
codex56
kimi-cli55
Use this skill when working with TypeScript code. AI agents frequently generate outdated patterns - using any instead of unknown, type assertions instead of satisfies, optional fields instead of discriminated unions, and missing strict mode options. This skill enforces modern TypeScript 5.x patterns.
Wrong (agents do this):
{
"compilerOptions": {
"strict": false,
"target": "ES2020"
}
}
Correct:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"target": "ES2022"
}
}
Why: Strict mode catches entire categories of bugs. noUncheckedIndexedAccess prevents unsafe array/object access. Agents often skip these for "convenience."
Wrong (agents do this):
const config = {
port: 3000,
host: "localhost",
} as Config;
config.port.toFixed(); // No error even if port could be string
Correct:
const config = {
port: 3000,
host: "localhost",
} satisfies Config;
config.port.toFixed(); // TypeScript knows port is number
Why: satisfies validates the type without widening it. as silences the compiler and can hide bugs. Use satisfies for validation, as only when you genuinely know more than the compiler.
Wrong (agents do this):
interface ApiResponse {
data?: User;
error?: string;
loading?: boolean;
}
Correct:
type ApiResponse =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
Why: Optional fields allow impossible states (data AND error both present). Discriminated unions make each state explicit and exhaustively checkable.
Wrong (agents do this):
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
};
// Type: { home: string; about: string; contact: string }
Correct:
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
} as const;
// Type: { readonly home: "/"; readonly about: "/about"; readonly contact: "/contact" }
Why: Without as const, TypeScript widens literal types to string. With it, you get exact literal types and readonly properties.
Wrong (agents do this):
function parseJson(text: string): any {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
data.nonExistent.method(); // No error - runtime crash
Correct:
function parseJson(text: string): unknown {
return JSON.parse(text);
}
const data = parseJson('{"name": "test"}');
if (isUser(data)) {
data.name; // Safe - type narrowed
}
Why: any disables all type checking. unknown forces you to narrow the type before using it, catching bugs at compile time.
Wrong (agents do this):
function getLocaleMessage(id: string): string { ... }
Correct:
type Locale = 'en' | 'ja' | 'pt';
type MessageKey = 'welcome' | 'goodbye';
type LocaleMessageId = `${Locale}_${MessageKey}`;
function getLocaleMessage(id: LocaleMessageId): string { ... }
Why: Template literal types create precise string patterns from unions. The compiler catches typos and invalid combinations at build time.
Wrong (agents do this):
function createLight<C extends string>(colors: C[], defaultColor?: C) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // No error - purple widens C
Correct:
function createLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { ... }
createLight(['red', 'green', 'blue'], 'purple'); // Error - 'purple' not in C
Why: NoInfer<T> (TypeScript 5.4+) prevents a parameter from influencing type inference, ensuring stricter checks.
Wrong (agents do this):
function getUser(id: string): User { ... }
function getOrder(id: string): Order { ... }
const userId = getUserId();
getOrder(userId); // No error - but wrong!
Correct:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
function getUser(id: UserId): User { ... }
function getOrder(id: OrderId): Order { ... }
const userId = getUserId();
getOrder(userId); // Error - UserId is not OrderId
Why: Branded types prevent accidentally passing one ID type where another is expected. The brand exists only at compile time - zero runtime cost.
Wrong (agents do this):
function handleStatus(status: "active" | "inactive" | "pending") {
switch (status) {
case "active":
return "Active";
case "inactive":
return "Inactive";
// 'pending' silently falls through
}
}
Correct:
function handleStatus(status: "active" | "inactive" | "pending") {
switch (status) {
case "active":
return "Active";
case "inactive":
return "Inactive";
case "pending":
return "Pending";
default: {
const _exhaustive: never = status;
throw new Error(`Unhandled status: ${_exhaustive}`);
}
}
}
Why: The never check ensures every union member is handled. When a new status is added, the compiler flags the missing case.
Wrong (agents do this):
function processItem(item: unknown) {
const user = item as User;
console.log(user.name);
}
Correct:
function isUser(item: unknown): item is User {
return typeof item === "object" && item !== null && "name" in item && "email" in item;
}
function processItem(item: unknown) {
if (isUser(item)) {
console.log(item.name); // Safe - narrowed to User
}
}
Why: Type predicates (item is User) narrow types safely with runtime checks. Type assertions (as User) bypass the compiler and can hide bugs.
Wrong (agents do this):
import { User, UserService } from "./user";
// User is only used as a type, but gets included in the bundle
Correct:
import type { User } from "./user";
import { UserService } from "./user";
Why: import type is erased at compile time, reducing bundle size. It also makes the intent clear - this import is for types only.
Wrong (agents do this):
interface Config {
[key: string]: string;
}
Correct:
type Config = Record<string, string>;
// Or better - use a specific union for keys:
type Config = Record<"host" | "port" | "env", string>;
Why: Record<K, V> is more readable and composable than index signatures. When possible, use a union for keys to get exhaustive checking.
Wrong (agents do this):
const file = openFile("data.txt");
try {
processFile(file);
} finally {
file.close();
}
Correct:
using file = openFile("data.txt");
processFile(file);
// file.close() called automatically via Symbol.dispose
Why: The using keyword (TypeScript 5.2+) provides deterministic resource cleanup via the Disposable protocol, similar to Python's with or C#'s using.
strict: true and noUncheckedIndexedAccess: true in every projectsatisfies for type validation without wideningtype or kind field for state modelingas const for configuration objects and route mapsimport type for all type-only importsswitch with never default for union handlingany - use unknown and narrow with type guardsas for type assertions unless you genuinely know more than the compiler// @ts-ignore or // @ts-expect-error without a comment explaining whyenum - use as const objects or union types insteadFunction type - use specific function signaturesWeekly Installs
59
Repository
GitHub Stars
1
First Seen
Feb 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode58
cursor57
gemini-cli56
cline56
codex56
kimi-cli55
Caveman-Review:AI代码审查工具,提升PR评论效率与代码质量
7,700 周安装