关于 typescript-types
typescript-types 是一项在当前任务中应用 TypeScript 类型约定的 Skill,适合编写或审查 TypeScript 代码的开发者。它强调窄化类型、不可变数据、安全处理未知值,并要求优先遵循仓库自身规范。
主要功能
- 01按仓库规范应用类型约定
- 02优先使用窄化类型与推断
- 03用只读类型减少意外修改
- 04使用模板字面量约束字符串
- 05以 unknown 处理不确定数据
- 06谨慎使用断言与错误抑制
适用场景
- 01为新代码选择合适的类型表达
- 02审查 TypeScript 类型安全性
- 03根据项目配置调整类型风格
- 04处理第三方类型定义不匹配
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mkosir/ typescript-style-guide --skill typescript-types安装后在 Claude Code 中输入/typescript-types确认已加载。
详细介绍
类型
在当前任务的上下文中,遵循 TypeScript 风格指南中的类型约定。
工作流
- 检查使用该代码的仓库中的约定和配置。
- 明确的仓库约定优先于这份带有主观倾向的指南。
- 仅应用、审查或说明与任务相关的指南内容。
- 当适用的模型取决于上下文或判断时,说明重要的权衡。
边界
- 让 TypeScript 和 ESLint 负责它们可以自动强制执行的检查。
- 不要仅仅因为启用了这项技能,就引入无关的 TypeScript 风格指南约定。
类型
创建类型时,请考虑如何才能最好地描述我们的代码。
表达力强且尽可能精确的类型能为代码库带来多项好处:
- 提高类型安全性——更精确的类型能提供关于数据形状和行为的更具体信息,从而在编译时发现错误。
- 提高代码清晰度——明确数据的边界和约束,降低认知负担,让其他开发者更容易理解代码。
- 更易于重构——使用更精确的类型,可以降低代码变更的风险,让你更有信心地进行重构。
类型推断
一般而言,仅在有助于缩窄类型时才显式声明类型。
不需要添加类型,并不意味着就不应该添加。在某些情况下,显式声明类型可以 提高代码可读性并明确意图。在有助于缩窄类型时,显式声明类型:
// ❌ Avoid
const employees = new Map(); // Inferred as wide type 'Map<any, any>'
employees.set('Lea', 17);
type UserRole = 'admin' | 'guest';
const [userRole, setUserRole] = useState('admin'); // Inferred as 'string', not the desired narrowed literal type
// ✅ Use explicit type declarations to narrow the types.
const employees = new Map<string, number>(); // Narrowed to 'Map<string, number>'
employees.set('Gabriel', 32);
type UserRole = 'admin' | 'guest';
const [userRole, setUserRole] = useState<UserRole>('admin'); // Explicit type 'UserRole'
如果类型可以推断得出,则避免显式声明:
// ❌ Avoid
const userRole: string = 'admin'; // Inferred as wide type 'string'
const employees = new Map<string, number>([['Gabriel', 32]]); // Redundant type declaration
const [isActive, setIsActive] = useState<boolean>(false); // Redundant, inferred as 'boolean'
// ✅ Use type inference.
const USER_ROLE = 'admin'; // Inferred as narrowed string literal type 'admin'
const employees = new Map([['Gabriel', 32]]); // Inferred as 'Map<string, number>'
const [isActive, setIsActive] = useState(false); // Inferred as 'boolean'
数据不可变性
不可变性应当是一项核心原则。尽可能使用 Readonly 和 ReadonlyArray 等类型来防止意外变更。
- 只读类型有助于防止意外变更,以及由非预期副作用引发的错误。这种保护仅在类型检查期间生效,不会在运行时冻结值。
- 处理数据时,始终返回新的数组、对象或其他基于引用的数据结构。为降低未来开发者的认知负担,尽量让数据对象保持扁平、简洁。
- 谨慎使用变更操作,仅在确有必要时使用,例如处理复杂对象或优化性能。
// ❌ Avoid data mutations
const removeFirstUser = (users: Array<User>) => {
if (users.length === 0) {
return users;
}
return users.splice(1);
};
// ✅ Use readonly type to prevent accidental mutations
const removeFirstUser = (users: ReadonlyArray<User>) => {
if (users.length === 0) {
return users;
}
return users.slice(1);
// Using arr.splice(1) errors - Function 'splice' does not exist on 'users'
};
使用 Satisfies 实现类型安全的常量
as const satisfies 语法将精确、只读的类型推断与类型验证结合起来。当常量需要保留其精确值,同时符合更宽泛的类型时,这种语法很有用。
主要优点:
- 使用
as const将值设为只读- 确保常量被视为只读。
- 将值的类型缩窄为其字面量类型,防止意外修改。
- 使用
satisfies进行验证- 确保对象符合更宽泛的类型,同时不会扩大其推断类型。
- 在保留精确推断类型的同时,帮助在编译时发现类型不匹配。
数组常量:
type UserRole = 'admin' | 'editor' | 'moderator' | 'viewer' | 'guest';
// ❌ Avoid constant of wide type
const DASHBOARD_ACCESS_ROLES: ReadonlyArray<UserRole> = ['admin', 'editor', 'moderator'];
// ❌ Avoid constant with incorrect values
const DASHBOARD_ACCESS_ROLES = ['admin', 'contributor', 'analyst'] as const;
// ✅ Use readonly constant of narrowed type
const DASHBOARD_ACCESS_ROLES = ['admin', 'editor', 'moderator'] as const satisfies ReadonlyArray<UserRole>;
对象常量:
type OrderStatus = {
pending: 'pending' | 'idle';
fulfilled: boolean;
error: string;
};
// ❌ Avoid mutable constant of wide type
const IDLE_ORDER: OrderStatus = {
pending: 'idle',
fulfilled: true,
error: 'Shipping Error',
};
// ❌ Avoid constant with incorrect values
const IDLE_ORDER = {
pending: 'done',
fulfilled: 'partially',
error: 116,
} as const;
// ✅ Use readonly constant of narrowed type
const IDLE_ORDER = {
pending: 'idle',
fulfilled: true,
error: 'Shipping Error',
} as const satisfies OrderStatus;
模板字面量类型
善用模板字面量类型,它们可以通过插值创建精确且类型安全的字符串结构。与宽泛的字符串类型相比,它们是强大的替代方案,能提供更好的类型安全性。
模板字面量类型会约束 TypeScript 在编译时已知的值。它们不会验证运行时收到的字符串。
采用模板字面量类型有多项优势:
- 防止因拼写错误或无效字符串导致的问题。
- 提供更好的类型安全性和自动补全支持。
- 提高代码的可维护性和可读性。
模板字面量类型适用于各种实际场景,例如:
-
字符串模式 - 使用模板字面量类型在类型检查期间强制执行特定的字符串模式。
// ❌ Avoid const appVersion = '2.6'; // ✅ Use type Version = `v${number}.${number}.${number}`; const appVersion: Version = 'v2.6.1'; -
API 端点 - 使用模板字面量类型将值限制为有效的 API 路由。
// ❌ Avoid const userEndpoint = '/api/usersss'; // Type 'string' - Typo 'usersss': the route doesn't exist, leading to a runtime error. // ✅ Use type ApiRoute = 'users' | 'posts' | 'comments'; type ApiEndpoint = `/api/${ApiRoute}`; // Type ApiEndpoint = "/api/users" | "/api/posts" | "/api/comments" const userEndpoint: ApiEndpoint = '/api/users'; -
国际化键 - 避免依赖原始字符串作为翻译键,因为这可能导致拼写错误和缺少翻译。使用模板字面量类型约束其结构。
// ❌ Avoid const homeTitle = 'translation.homesss.title'; // Type 'string' - Typo 'homesss': the translation doesn't exist, leading to a runtime error. // ✅ Use type LocaleKeyPages = 'home' | 'about' | 'contact'; type TranslationKey = `translation.${LocaleKeyPages}.${string}`; // Type TranslationKey = `translation.home.${string}` | `translation.about.${string}` | `translation.contact.${string}` const homeTitle: TranslationKey = 'translation.home.title'; -
CSS 工具 - 避免使用原始字符串表示颜色值,因为这可能导致无效或不存在的颜色。使用模板字面量类型强制采用已知颜色名称,并要求自定义值以
#开头。// ❌ Avoid const color = 'blue-450'; // Type 'string' - Color 'blue-450' doesn't exist, leading to a runtime error. // ✅ Use type BaseColor = 'blue' | 'red' | 'yellow' | 'gray'; type Variant = 50 | 100 | 200 | 300 | 400; type Color = `${BaseColor}-${Variant}` | `#${string}`; // Type Color = "blue-50" | "blue-100" | "blue-200" ... | "red-50" | "red-100" ... | #${string} const iconColor: Color = 'blue-400'; const customColor: Color = '#AD3128'; -
数据库查询 - 避免使用原始字符串表示表名或列名,因为这可能导致拼写错误。使用模板字面量类型定义有效的表和列组合。
// ❌ Avoid
const query = 'SELECT name FROM usersss WHERE age > 30'; // Type 'string' - Typo 'usersss': table doesn't exist, leading to a runtime error.
// ✅ Use
type Table = 'users' | 'posts' | 'comments';
type Column<TTableName extends Table> =
TTableName extends 'users' ? 'id' | 'name' | 'age' :
TTableName extends 'posts' ? 'id' | 'title' | 'content' :
TTableName extends 'comments' ? 'id' | 'postId' | 'text' :
never;
type Query<TTableName extends Table> = `SELECT ${Column<TTableName>} FROM ${TTableName} WHERE ${string}`;
const userQuery: Query<'users'> = 'SELECT name FROM users WHERE age > 30'; // Accepted by Query<'users'>
const invalidQuery: Query<'users'> = 'SELECT title FROM users WHERE age > 30'; // Error: 'title' is not a column in 'users' table.
any 和 unknown 类型
不得使用 any 类型,因为它会绕过类型检查,并允许不安全的操作和赋值。这可能掩盖严重的编程错误。
处理含义不明确的数据时,请使用 unknown,它是 any 的类型安全对应项。
任何值都可以赋给 unknown,但在访问其属性或将其赋给更具体的类型之前,必须先进行类型收窄。
// ❌ Avoid any
const foo: any = 'five';
const bar: number = foo; // no type error
// ✅ Use unknown
const foo: unknown = 5;
const bar: number = foo; // type error - Type 'unknown' is not assignable to type 'number'
// Narrow the type before dereferencing it using:
// Type guard
const isNumber = (num: unknown): num is number => {
return typeof num === 'number';
};
if (!isNumber(foo)) {
throw Error(`API provided a fault value for field 'foo':${foo}. Should be a number!`);
}
const bar: number = foo;
类型断言与非空断言
类型断言 user as User 和非空断言 user!.name 都不安全。两者只会让 TypeScript 编译器不再报错,并增加应用在运行时崩溃的风险。
只有在特殊情况下才能使用,例如第三方库的类型不匹配,并且必须充分说明将其引入代码库的理由。
type User = { id: string; username: string; avatar: string | null };
// ❌ Avoid type assertions
const user = { name: 'Nika' } as User;
// ❌ Avoid non-nullability assertions
const getUsername = (user: User | null) => user!.username; // Runtime error when user is null
类型错误
无法消除 TypeScript 错误时,最后才使用 @ts-expect-error 将其屏蔽。
当被屏蔽的错误不再存在时,该指令会通知编译器,确保错误过时后能够重新审查。与之不同的是,@ts-ignore 即使在错误解决后也可能悄然遗留。
- 使用
@ts-expect-error时,务必附上清晰的说明,解释使用它的必要性。 - 避免使用
@ts-ignore,因为它不会跟踪被屏蔽的错误。
{'@typescript-eslint/ban-ts-comment': [ 'error', { 'ts-expect-error': 'allow-with-description' }, ]}
// ❌ Avoid @ts-ignore as it will do nothing if the following line is error-free.
// @ts-ignore
const newUser = createUser('Gabriel');
// ✅ Use @ts-expect-error with description.
// @ts-expect-error: This library function has incorrect type definitions - createUser accepts string as an argument.
const newUser = createUser('Gabriel');
类型定义
TypeScript 提供了两种定义类型的方式:type 和 interface。虽然这两种方式在功能上有所不同,但在大多数情况下可以互换使用。为保持一致性,请选择其中一种并始终如一地使用。
<Rule prefix="使用类型别名定义所有类型" href="https://typescript-eslint.io/rules/consistent-type-definitions"
如果要开发未来可能由第三方使用者扩展的包,或者团队更倾向于使用接口,请考虑使用接口。在这些情况下,可以根据需要停用 lint 规则,例如定义类型联合时(如 `type Status = 'loading' | 'error'`)。{
'@typescript-eslint/consistent-type-definitions': ['error', 'type']}
// ❌ Avoid interface definitions
interface UserRole = 'admin' | 'guest'; // Invalid - interfaces can't define type unions
interface UserInfo {
name: string;
role: 'admin' | 'guest';
}
// ✅ Use type definition
type UserRole = 'admin' | 'guest';
type UserInfo = {
name: string;
role: UserRole;
};
执行声明合并时(例如扩展第三方库类型),请使用 interface,并在必要时停用 lint 规则。
// types.ts
declare namespace NodeJS {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface ProcessEnv {
NODE_ENV: 'development' | 'production';
PORT: string;
CUSTOM_ENV_VAR: string;
}
}
// server.ts
app.listen(process.env.PORT, () => {...}
数组类型
<Rule prefix="数组类型应使用泛型语法定义" href="https://typescript-eslint.io/rules/array-type/#generic"
{
'@typescript-eslint/array-type': ['error', { default: 'generic' }]}
由于 'generic' 和 'array' 定义在功能上没有区别,可以选择团队认为最易读的写法。
// ❌ Avoid
const x: string[] = ['foo', 'bar'];
const y: readonly string[] = ['foo', 'bar'];
// ✅ Use
const x: Array<string> = ['foo', 'bar'];
const y: ReadonlyArray<string> = ['foo', 'bar'];
类型导入和导出
TypeScript 允许在导入语句中指定 type 关键字,以表明该导出仅存在于类型系统中,而非运行时。
类型导入必须始终单独声明:
- Tree Shaking 和死代码消除——
import type会在编译期间被擦除,不会留下供打包工具分析或移除的运行时导入。 - 避免副作用——根据编译器设置,仅用作类型的常规导入可能会保留在生成的 JavaScript 中并触发模块副作用。
- 代码清晰度——明确区分运行时导入和仅用于类型的导入。
{'@typescript-eslint/consistent-type-imports': 'error'}
// ❌ Avoid using `import` for both runtime and type
import { MyClass } from 'some-library';
// Even if MyClass is only used as a type, a regular import can pull the module into the runtime bundle.
// ✅ Use `import type`
import type { MyClass } from 'some-library';
// This import is removed from the emitted JavaScript.
服务和类型生成
文档一经编写就会过时,而错误的文档比没有文档更糟糕。描述应用交互模块(例如 API、消息协议和数据库)的类型也是如此。
对于 REST、GraphQL 和 MQ 等外部服务,必须根据其契约生成类型,无论契约采用 Swagger、Schema 还是其他形式(例如 openapi-ts、graphql-config)。避免手动声明和维护类型,因为它们很容易与实际情况脱节。
生成的类型可使编译时契约保持同步,但不会在运行时验证从外部服务接收的数据。
例外情况下,仅在没有其他选择时才手动声明类型,例如服务没有文档、无法获取数据以检索契约,或无法访问数据库来推断类型。