Loading...
Loading...
Compare original and translation side by side
ts-best-practices-functional| Element | Convention | Example |
|---|---|---|
| Files | | |
| Variables | | |
| Functions | | |
| Types/interfaces | | |
| Constants | | |
| Const objects | | |
ts-best-practices-functional.ts@pkg/*function// === HELPERS ===| 元素 | 约定 | 示例 |
|---|---|---|
| 文件 | | |
| 变量 | | |
| 函数 | | |
| 类型/接口 | | |
| 常量 | | |
| 常量对象 | 键使用 | |
ParamsOptionsArgs| Suffix | Use case |
|---|---|
| Required input parameters |
| Optional configuration |
| Function arguments (less common) |
.ts@pkg/*function// === HELPERS ===@param@returns@example/**
* Parses raw webhook headers into a typed structure.
*
* @param params - The raw headers and provider identifier
* @returns Parsed webhook headers or a parse error
*
* @example
* ```ts
* const result = parseWebhookHeaders({ headers, provider: 'github' })
* ```
*/
export function parseWebhookHeaders(params: ParseHeadersParams) {
// ...
}@private/**
* Extracts the display name from a user record.
*
* @private
*/
function getDisplayName(user: User): string {
return user.displayName ?? user.email
}ParamsOptionsArgs| 后缀 | 使用场景 |
|---|---|
| 必填输入参数 |
| 可选配置参数 |
| 函数参数(较少使用) |
type AuthStrategy =
| { strategy: 'connection'; connectionId: string }
| { strategy: 'integration'; integrationId: string }
| { strategy: 'user'; userId: string; token: string }userIdorgIdtype Brand<T, B> = T & { __brand: B }
type UserId = Brand<string, 'UserId'>
type OrgId = Brand<string, 'OrgId'>
function userId(id: string): UserId {
return id as UserId
}as constconst STATUSES = ['pending', 'active', 'completed'] as const
type Status = (typeof STATUSES)[number] // "pending" | "active" | "completed"type-festSetRequiredSetOptionalPartialDeepReadonlyDeepExceptSimplifyanyunknown@param@returns@example/**
* 将原始Webhook头解析为类型化结构。
*
* @param params - 原始头信息和提供商标识符
* @returns 解析后的Webhook头或解析错误
*
* @example
* ```ts
* const result = parseWebhookHeaders({ headers, provider: 'github' })
* ```
*/
export function parseWebhookHeaders(params: ParseHeadersParams) {
// ...
}@private/**
* 从用户记录中提取显示名称。
*
* @private
*/
function getDisplayName(user: User): string {
return user.displayName ?? user.email
}| Scenario | Use | Why |
|---|---|---|
| Early return / guard | | Cleaner guard clauses |
| Simple A or B | Single inline ternary or a tiny | Lightweight, no library churn |
| 3+ branches | | Exhaustive, readable |
| Discriminated unions | | Compile-time safety |
type AuthStrategy =
| { strategy: 'connection'; connectionId: string }
| { strategy: 'integration'; integrationId: string }
| { strategy: 'user'; userId: string; token: string }userIdorgIdtype Brand<T, B> = T & { __brand: B }
type UserId = Brand<string, 'UserId'>
type OrgId = Brand<string, 'OrgId'>
function userId(id: string): UserId {
return id as UserId
}as constconst STATUSES = ['pending', 'active', 'completed'] as const
type Status = (typeof STATUSES)[number] // "pending" | "active" | "completed"type-festSetRequiredSetOptionalPartialDeepReadonlyDeepExceptSimplifyanyunknown| Avoid | Use instead |
|---|---|
| |
| Ternaries (esp. nested) | |
| |
Banner comments ( | Let file structure speak |
| Arrow functions for private helpers | |
Type assertion ( | Type guard or schema parse |
Concatenated property names ( | Nested objects ( |
| 5+ positional params | Refactor to a |
| 场景 | 使用方式 | 原因 |
|---|---|---|
| 提前返回/守卫 | | 更简洁的守卫逻辑 |
| 简单二选一 | 单行三元表达式或简短的 | 轻量,无需引入额外库 |
| 3个及以上分支 | | 穷举性,可读性强 |
| 可区分联合 | | 编译时安全检查 |
import type { User } from './types'
interface FetchUserParams {
userId: UserId
}
interface FetchUserOptions {
cache?: boolean
includeDeleted?: boolean
}
/**
* Fetches a user by ID, optionally with caching.
*
* @param params - Required: the user ID to fetch
* @param options - Optional: cache + soft-delete flags
* @returns The user record, or null if not found
*
* @example
* ```ts
* const user = await fetchUser({ userId }, { cache: true })
* ```
*/
export async function fetchUser(
{ userId }: FetchUserParams,
options?: FetchUserOptions
): Promise<User | null> {
if (options?.cache) {
const cached = await readCache(userId)
if (cached) return cached
}
return loadUser(userId, options?.includeDeleted ?? false)
}
/**
* @private
*/
function readCache(userId: UserId): Promise<User | null> {
// ...
}*Params*OptionsUserId@examplefunction renderStatus(deployment: Deployment) {
if (deployment.status === 'building') {
return 'Building...'
} else if (deployment.status === 'ready') {
if (deployment.error) {
return `Failed: ${deployment.error}`
} else {
return `Ready at ${deployment.url}`
}
} else {
return 'Unknown'
}
}import { match, P } from 'ts-pattern'
function renderStatus(deployment: Deployment): string {
return match(deployment)
.with({ status: 'building' }, () => 'Building...')
.with({ status: 'ready', error: P.nullish }, (d) => `Ready at ${d.url}`)
.with({ status: 'ready' }, (d) => `Failed: ${d.error}`)
.exhaustive()
}| 需避免的写法 | 替代方案 |
|---|---|
| |
| 三元表达式(尤其是嵌套的) | 二选一用 |
| |
横幅注释(如 | 依靠文件结构来区分 |
| 私有辅助函数使用箭头函数 | |
无验证的类型断言( | 类型守卫或模式解析 |
拼接属性名(如 | 嵌套对象(如 |
| 5个及以上位置参数 | 重构为 |
| Skipped rule | Verbatim excuse | Why it's wrong |
|---|---|---|
Use | "im in a meeting in 5 min, just inline the strings — I'll refactor later" | "Later" never comes. Positional args swap silently at the call site (email vs name vs roleId), and the cost of the interface is one block of text that pays for itself the first time the signature changes. |
| JSDoc on every exported function | "the change is tiny so I skipped JSDoc — the function name is self-documenting" | Names describe what, not why. The next reader (or model) loses the intent — and exports are the API surface, so docs there are highest-leverage. Add the JSDoc before merging, not after. |
Branded types for IDs ( | "they're both strings basically, branded types feel over-engineered for a fetch" | "Basically" is the rationalization. Real bugs from swapping |
| "the existing | The reviewer complains when a new variant is added and the |
Never use | "this is internal/utility code, | |
import type { User } from './types'
interface FetchUserParams {
userId: UserId
}
interface FetchUserOptions {
cache?: boolean
includeDeleted?: boolean
}
/**
* 根据ID获取用户,可选启用缓存。
*
* @param params - 必填:要获取的用户ID
* @param options - 可选:缓存和软删除标记
* @returns 用户记录,若未找到则返回null
*
* @example
* ```ts
* const user = await fetchUser({ userId }, { cache: true })
* ```
*/
export async function fetchUser(
{ userId }: FetchUserParams,
options?: FetchUserOptions
): Promise<User | null> {
if (options?.cache) {
const cached = await readCache(userId)
if (cached) return cached
}
return loadUser(userId, options?.includeDeleted ?? false)
}
/**
* @private
*/
function readCache(userId: UserId): Promise<User | null> {
// ...
}*Params*OptionsUserId@examplefunction renderStatus(deployment: Deployment) {
if (deployment.status === 'building') {
return 'Building...'
} else if (deployment.status === 'ready') {
if (deployment.error) {
return `Failed: ${deployment.error}`
} else {
return `Ready at ${deployment.url}`
}
} else {
return 'Unknown'
}
}import { match, P } from 'ts-pattern'
function renderStatus(deployment: Deployment): string {
return match(deployment)
.with({ status: 'building' }, () => 'Building...')
.with({ status: 'ready', error: P.nullish }, (d) => `Ready at ${d.url}`)
.with({ status: 'ready' }, (d) => `Failed: ${d.error}`)
.exhaustive()
}| 被跳过的规则 | 原话借口 | 错误原因 |
|---|---|---|
对≥2个参数的函数使用 | "我5分钟后要开会,直接写字符串参数就行——之后再重构" | "之后"永远不会到来。位置参数在调用时会被无声地交换(比如email、name、roleId的顺序),而接口的成本只是一段文本,在签名第一次变更时就能收回成本。 |
| 每个导出函数都添加JSDoc | "改动很小,所以我跳过了JSDoc——函数名称已经自解释了" | 名称描述的是做什么,而非为什么。下一个读者(或模型)会失去上下文意图——而导出项是API接口,所以这里的文档价值最高。合并前就添加JSDoc,不要事后补。 |
对ID使用品牌类型( | "它们本质上都是字符串,品牌类型对于一个获取函数来说有点过度设计了" | "本质上"是一种合理化借口。将 |
对≥3个分支使用 | "现有的 | 当添加新变体时, |
绝对不要使用 | "这是内部/工具代码,用 | |