mcpField
T
typescript-types

typescript-types

github.com/mkosir其他78540 次安装8 周前更新
代码仓库

关于 typescript-types

typescript-types 是一项在当前任务中应用 TypeScript 类型约定的 Skill,适合编写或审查 TypeScript 代码的开发者。它强调窄化类型、不可变数据、安全处理未知值,并要求优先遵循仓库自身规范。

主要功能

  1. 01按仓库规范应用类型约定
  2. 02优先使用窄化类型与推断
  3. 03用只读类型减少意外修改
  4. 04使用模板字面量约束字符串
  5. 05以 unknown 处理不确定数据
  6. 06谨慎使用断言与错误抑制

适用场景

  1. 01为新代码选择合适的类型表达
  2. 02审查 TypeScript 类型安全性
  3. 03根据项目配置调整类型风格
  4. 04处理第三方类型定义不匹配

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mkosir/typescript-style-guide --skill typescript-types

安装后在 Claude Code 中输入/typescript-types确认已加载。

详细介绍

类型

在当前任务的上下文中,遵循 TypeScript 风格指南中的类型约定。

工作流

  1. 检查使用该代码的仓库中的约定和配置。
  2. 明确的仓库约定优先于这份带有主观倾向的指南。
  3. 仅应用、审查或说明与任务相关的指南内容。
  4. 当适用的模型取决于上下文或判断时,说明重要的权衡。

边界

  • 让 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"

{'@typescript-eslint/consistent-type-definitions': ['error', 'type']}

如果要开发未来可能由第三方使用者扩展的包,或者团队更倾向于使用接口,请考虑使用接口。在这些情况下,可以根据需要停用 lint 规则,例如定义类型联合时(如 `type Status = 'loading' | 'error'`)。
// ❌ 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)。避免手动声明和维护类型,因为它们很容易与实际情况脱节。

生成的类型可使编译时契约保持同步,但不会在运行时验证从外部服务接收的数据。

例外情况下,仅在没有其他选择时才手动声明类型,例如服务没有文档、无法获取数据以检索契约,或无法访问数据库来推断类型。

评论

支持 Markdown