BlogBlog · clean-code

Đặt tên trong code: quy tắc cụ thể cho biến, hàm, boolean, đơn vị đo và type trong TypeScript

Bộ quy tắc đặt tên dùng được ngay: tên theo domain thay vì data/info, boolean is/has/can, động từ get/fetch/find/compute, đơn vị trong tên hoặc branded type, glossary cho thuật ngữ tiếng Việt, và cách ép quy tắc bằng ESLint naming-convention.

Part of Developer Tips series
March 4, 2026
18 min read
By Leo Pham
clean-codetypescriptnamingcode-qualityrefactoring
Share this post:

Có lần mình mất gần một buổi chiều để debug một khoản phí ship bị nhân 1000. Nguyên nhân: hàm calcFee(weight, timeout) nhận weight tính bằng kg, nhưng một chỗ gọi truyền gram vì biến bên đó tên là weight luôn. Không có test nào sai, không có type nào sai. Chỉ có một cái tên thiếu đơn vị.

Đặt tên không phải chuyện thẩm mỹ. Tên là phần duy nhất của code mà người đọc đọc trước khi hiểu logic, nên một tên mơ hồ buộc họ phải mở implementation ra xem. Nhân với mỗi lần đọc, mỗi người trong team, đó là thời gian thật. Tệ hơn, tên sai dẫn tới bug thật như ví dụ trên.

Bài này không nhắc lại "hãy đặt tên có ý nghĩa". Thay vào đó là các quy tắc cụ thể mình áp dụng trong codebase TypeScript: tên theo domain, boolean, động từ cho hàm, đơn vị đo, type, dự án có thuật ngữ tiếng Việt, và cách để ESLint với công cụ refactor giữ quy tắc thay vì trông vào trí nhớ của reviewer.

Đặt tên theo ý định và domain, không theo kiểu dữ liệu

Các tên data, info, item, temp, result, obj, list nói cho bạn biết hình dạng của giá trị, thứ TypeScript đã nói rồi. Cái người đọc cần là ý nghĩa: đây là cái gì trong nghiệp vụ.

interface Order {
  id: string
  status: 'pending' | 'paid' | 'cancelled'
  totalVnd: number
  customerId: string
}

// Trước: đọc xong vẫn không biết hàm làm gì
function process(data: Order[]) {
  const result = data.filter((item) => item.status === 'paid')
  const temp = result.reduce((acc, item) => acc + item.totalVnd, 0)
  return { result, temp }
}

// Sau: tên kể lại nghiệp vụ
function summarizePaidOrders(orders: Order[]) {
  const paidOrders = orders.filter((order) => order.status === 'paid')
  const revenueVnd = paidOrders.reduce((sum, order) => sum + order.totalVnd, 0)
  return { paidOrders, revenueVnd }
}

// "response" và "json" là tên theo tầng kỹ thuật, ổn trong vài dòng sát biên
async function fetchOrder(orderId: string): Promise<unknown> {
  const response = await fetch(`/api/orders/${encodeURIComponent(orderId)}`)
  const json: unknown = await response.json()
  return json // sau bước validate, đặt tên lại thành order
}

Mẹo kiểm tra nhanh: đọc to tên biến cùng dòng code dùng nó. if (result.length > 0) không nói gì. if (paidOrders.length > 0) là một câu tiếng Anh hoàn chỉnh.

Vài tên kỹ thuật vẫn ổn: response, request, error, event ở sát biên I/O, vì ở đó ý nghĩa đúng là "response HTTP". Vấn đề chỉ xuất hiện khi những tên đó trôi sâu vào logic nghiệp vụ.

Độ dài tên tỷ lệ với phạm vi

Tên càng sống lâu và được dùng càng xa chỗ khai báo thì càng phải dài và đầy đủ.

  • Biến trong một vòng lặp ba dòng: i, order là đủ.
  • Biến cục bộ trong hàm 30 dòng: paidOrders, shippingFeeVnd.
  • Export từ module, dùng ở 20 file: calculateShippingFeeVnd, DEFAULT_PAYMENT_TIMEOUT_MS.

Viết tắt: chỉ dùng loại cả ngành đều biết (id, url, api, db, http, json, html, ctx trong middleware, req/res trong Express handler). Tự chế viết tắt như usr, cnt, amt, ordSt buộc người đọc giải mã. Nếu domain có viết tắt riêng (sku, qty, cod), đưa nó vào glossary của dự án (phần dưới) để nó thành "đã biết" một cách chính thức.

Với chữ viết tắt, chọn một kiểu viết hoa và giữ nhất quán: mình dùng userId, parseHtml, apiUrl (chỉ viết hoa chữ đầu) vì kiểu userID, parseHTML dễ ra những tên như HTTPAPIURL không đọc nổi.

Boolean: tiền tố is/has/can/should, không phủ định, không làm tham số

Boolean nên đọc được như một câu hỏi có/không. Bốn tiền tố mình dùng:

  • is: trạng thái (isPaid, isLoading, isExpired)
  • has: sở hữu hoặc tồn tại (hasDiscount, hasUnreadMessages)
  • can: quyền hoặc khả năng (canRefund, canEditInvoice)
  • should: quyết định sẽ làm gì (shouldRetry, shouldSendReceipt)
// Sai: phủ định chồng phủ định
const isNotDisabled = true
if (!isNotDisabled) {
  // "nếu không phải không bị disable"... đọc ba lần mới hiểu
}

// Đúng: đặt tên theo trạng thái dương
const isEnabled = true
if (!isEnabled) {
  // rõ ràng
}

// Sai: boolean parameter, chỗ gọi không đọc được
declare function createUserBad(email: string, sendEmail: boolean, isAdmin: boolean): Promise<void>
await createUserBad('an@shop.vn', true, false) // true là gì? false là gì?

// Đúng: options object, tên xuất hiện ngay ở chỗ gọi
interface CreateUserOptions {
  sendWelcomeEmail?: boolean
  role?: 'admin' | 'member'
}
async function createUser(email: string, options: CreateUserOptions = {}): Promise<void> {
  const { sendWelcomeEmail = true, role = 'member' } = options
  console.log(email, sendWelcomeEmail, role)
}
await createUser('an@shop.vn', { sendWelcomeEmail: false })

Boolean parameter có hai vấn đề. Chỗ gọi createUser(email, true, false) không đọc được nếu không mở signature. Và khi cần thêm cờ thứ ba, bạn phải sửa mọi chỗ gọi. Options object giải quyết cả hai, đồng thời cho phép default rõ ràng. Một quy tắc kèm theo: nếu hai boolean loại trừ nhau (isAdmin, isMember), đó là một union role: 'admin' | 'member' đội lốt.

Ngoại lệ: props của component mô phỏng thuộc tính DOM (disabled, checked, open, required) giữ nguyên tên DOM. Viết <Button isDisabled> trong khi mọi <button> dùng disabled chỉ làm người dùng component phải nhớ thêm một biến thể.

Động từ cho hàm: thống nhất get, fetch, load, find, compute, build

Động từ đầu tên hàm là lời hứa về hành vi: có I/O không, có thể trả về rỗng không, có tốn kém không, có side effect không. Lời hứa chỉ có giá trị khi cả codebase giữ cùng một nghĩa. Đây là bảng quy ước mình đề xuất, bạn có thể sửa nhưng hãy viết nó ra:

Động từ Hứa hẹn Ví dụ
get Đồng bộ hoặc rẻ, không I/O mạng, luôn có giá trị getOrderTotalVnd(order)
fetch Gọi HTTP, async, có thể lỗi mạng fetchExchangeRates()
load Đọc từ DB, file, cache; async loadInvoiceTemplate(name)
find Tìm kiếm, có thể trả null/undefined findUserByEmail(email)
require / ...OrThrow Như find nhưng ném lỗi nếu không có requireOrder(orderId)
compute / calculate Thuần CPU, không side effect, có thể tốn calculateShippingFeeVnd(input)
build / to Dựng object mới trong bộ nhớ, không lưu buildInvoicePdf(order), toOrderDto(order)
create / save / delete Ghi xuống storage, có side effect createOrder(input)
interface User {
  id: string
  email: string
}
declare const db: {
  users: { findFirst(args: { where: { email: string } }): Promise<User | null> }
}

// find: có thể không có, return type nói rõ điều đó
async function findUserByEmail(email: string): Promise<User | null> {
  return db.users.findFirst({ where: { email: email.toLowerCase() } })
}

// require: caller không phải tự check null
async function requireUserByEmail(email: string): Promise<User> {
  const user = await findUserByEmail(email)
  if (!user) throw new Error(`User not found: ${email}`)
  return user
}

// calculate: thuần, test được không cần mock
function calculateShippingFeeVnd(weightGrams: number, distanceKm: number): number {
  const baseFeeVnd = 15_000
  const perKgVnd = 5_000
  const weightKg = Math.ceil(weightGrams / 1000)
  return baseFeeVnd + weightKg * perKgVnd + (distanceKm > 30 ? 10_000 : 0)
}

Trong React, có thêm một cặp quy ước: onX là tên prop (component cha truyền callback vào), handleX là tên hàm xử lý bên trong component. Cặp này giúp đọc JSX biết ngay đâu là API của component và đâu là logic nội bộ.

import { useState, type FormEvent } from 'react'

interface CouponFormProps {
  onApply: (code: string) => Promise<void>
  onCancel?: () => void
}

export function CouponForm({ onApply, onCancel }: CouponFormProps) {
  const [couponCode, setCouponCode] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setIsSubmitting(true)
    try {
      await onApply(couponCode.trim().toUpperCase())
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={couponCode} onChange={(event) => setCouponCode(event.target.value)} />
      <button type="submit" disabled={isSubmitting}>Áp dụng</button>
      {onCancel && <button type="button" onClick={onCancel}>Huỷ</button>}
    </form>
  )
}

Đặt tên handler theo ý định khi có thể (handleApplyCoupon) thay vì theo sự kiện DOM (handleClick) nếu component có nhiều nút. handleClick2 là dấu hiệu bạn nên đổi.

Đơn vị đo, ngày giờ và collection

Quay lại bug phí ship ở đầu bài. Mọi con số có đơn vị đều nên mang đơn vị trong tên: timeoutMs, ttlSeconds, sizeBytes, weightGrams, distanceKm, priceVnd, discountPercent. Chi phí là vài ký tự; lợi ích là chỗ gọi calculateShippingFeeVnd(weightKg, ...) sai đơn vị sẽ lộ ngay khi đọc.

Khi một loại đơn vị quan trọng tới mức nhầm là mất tiền (tiền tệ, thời gian timeout của payment), dùng branded type để compiler bắt lỗi thay vì mắt người:

declare const brand: unique symbol
type Brand<T, B extends string> = T & { readonly [brand]: B }

type Milliseconds = Brand<number, 'Milliseconds'>
type Vnd = Brand<number, 'Vnd'>

const ms = (value: number) => value as Milliseconds
const seconds = (value: number) => (value * 1000) as Milliseconds
const vnd = (value: number) => {
  if (!Number.isInteger(value)) throw new Error('VND không có phần lẻ')
  return value as Vnd
}

function withTimeout<T>(promise: Promise<T>, timeout: Milliseconds): Promise<T> {
  return Promise.race([
    promise,
    new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)),
  ])
}

declare const paymentRequest: Promise<{ transactionId: string }>
await withTimeout(paymentRequest, seconds(30)) // OK
// await withTimeout(paymentRequest, 30) -> lỗi: number không phải Milliseconds

// Phép toán trả về number thường, cần brand lại một cách có chủ đích
const subtotal = vnd(250_000)
const total = vnd(subtotal + 15_000)

Brand chỉ tồn tại lúc compile, không tốn byte nào ở runtime. Cái giá là phép toán trả về number trần nên bạn phải bọc lại, và dữ liệu đi qua JSON.parse mất brand, nên brand nên được gắn ở chỗ validate input (ví dụ trong zod schema bằng .transform(vnd)). Đừng brand mọi thứ: dành nó cho tiền, thời gian và ID của các entity hay bị truyền nhầm (OrderId vs CustomerId).

Ngày giờ. Quy ước mình dùng:

  • ...At cho một thời điểm tuyệt đối (instant, có timezone): createdAt, paidAt, expiresAt. Lưu UTC, kiểu Date hoặc ISO string có Z.
  • ...Date hoặc ...On cho ngày lịch không có giờ: birthDate, dueDate, deliveredOn. Kiểu string YYYY-MM-DD, không bao giờ là Date (vì new Date('2026-03-04') là nửa đêm UTC, tức 7 giờ sáng ở Việt Nam, và sẽ lệch ngày khi format ở timezone âm).
  • Khoảng thời gian luôn kèm đơn vị: ttlSeconds, retryDelayMs, không bao giờ chỉ là ttl.

Collection. Mảng dùng số nhiều (orders, orderIds), không thêm hậu tố List/Array vì type đã nói rồi. Map hoặc object tra cứu dùng xByY: orderById, ordersByCustomerId (giá trị là mảng thì orders số nhiều). Số đếm dùng orderCount thay vì numOrders hay ordersLength. Set dùng số nhiều kèm ý nghĩa: selectedOrderIds.

Đặt tên type trong TypeScript

// Không dùng tiền tố I: interface hay type alias là chi tiết implementation
interface IOrderBad {
  id: string
}
interface Order {
  id: string
  status: OrderStatus
}

// Union literal thay cho enum trong phần lớn trường hợp
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled'

// Khi cần danh sách giá trị lúc runtime (dropdown, validate), dùng object as const
export const PaymentMethod = {
  Cod: 'cod',
  BankTransfer: 'bank_transfer',
  Card: 'card',
} as const
export type PaymentMethod = (typeof PaymentMethod)[keyof typeof PaymentMethod]
// "cod" | "bank_transfer" | "card"

// Props của component: <TênComponent>Props
interface OrderTableProps {
  orders: readonly Order[]
  onSelectOrder: (orderId: string) => void
}

// Generic: một tham số thì T; nhiều tham số thì T + tên có nghĩa
interface Paginated<TItem> {
  items: TItem[]
  nextCursor: string | null
}
type ApiResponse<TData, TError = { code: string; message: string }> =
  | { ok: true; data: TData }
  | { ok: false; error: TError }

Lý do cụ thể cho từng quy ước:

  • Bỏ tiền tố I. Nó đến từ C#/Java nơi interface và class cùng tồn tại ở runtime. Trong TypeScript, người dùng type không cần biết đó là interface hay type, và bạn sẽ đổi qua lại giữa hai thứ đó mà không muốn đổi tên. Coding guidelines cho contributor của chính repo TypeScript ghi rõ "Do not use I as a prefix for interface names", và docs của rule naming-convention có sẵn ví dụ cấm tiền tố này.
  • Union literal thay enum. enum sinh code JavaScript thật (một object), không phải "erasable syntax". Flag erasableSyntaxOnly (TS 5.8) cấm enum, và type stripping của Node.js cũng không chạy được enum. Union literal so sánh trực tiếp với string từ API/DB, không cần map. Khi cần danh sách lúc runtime, pattern as const ở trên cho cả hai thứ với cùng một tên.
  • XxxProps để tìm nhanh bằng editor và để không đụng tên với chính component.
  • Tên generic. T, K, V đủ cho helper ngắn. Khi có từ hai tham số trở lên hoặc tham số sống qua nhiều dòng, TItem, TData, TError đọc dễ hơn hẳn T, U, V.

Dự án tiếng Việt: trộn ngôn ngữ và glossary

Đây là phần ít bài viết nhắc tới nhưng mình gặp ở gần như mọi dự án trong nước. Codebase có tongTien, ngayTao, trangThaiDonHang nằm cạnh createdAt và totalAmount. Hoặc tệ hơn: tổngTiền có dấu.

Quy tắc mình áp dụng:

  1. Không dùng identifier có dấu. JavaScript cho phép (const tổngTiền = 0 là hợp lệ), nhưng ECMAScript không chuẩn hoá Unicode khi so sánh identifier. Bộ gõ có thể sinh dạng dựng sẵn (NFC) hoặc tổ hợp (NFD), nên hai chữ tổngTiền trông giống hệt nhau có thể là hai biến khác nhau. Grep, autocomplete và bàn phím của đồng nghiệp nước ngoài cũng gặp khó.
  2. Mặc định dùng tiếng Anh cho khái niệm kỹ thuật và khái niệm phổ quát: order, invoice, customer, totalVnd, createdAt.
  3. Thuật ngữ đặc thù Việt Nam không có từ tiếng Anh khớp nghĩa thì chọn một tên, ghi vào glossary, và dùng duy nhất tên đó. Đừng để province, tinh, city cùng chỉ một khái niệm trong ba module.
  4. Không trộn trong cùng một tên: tongAmount, getDonHang là tệ nhất của cả hai thế giới.

Glossary là một file trong repo (ví dụ docs/glossary.md), được review như code. Một đoạn ví dụ:

Thuật ngữ nghiệp vụ Tên trong code Ghi chú
Tỉnh / thành phố trực thuộc trung ương province Dùng cho cả "thành phố" cấp tỉnh như Hà Nội, TP.HCM
Phường / xã / đặc khu ward Từ 1/7/2025 chính quyền địa phương còn hai cấp, không có cấp huyện. Schema mới không nên có district bắt buộc
Số CCCD / số định danh cá nhân nationalIdNumber 12 chữ số, lưu dạng string (có số 0 ở đầu), coi là dữ liệu cá nhân nhạy cảm
Mã số thuế taxCode String, không phải number
Thu hộ (COD) cod Viết tắt đã có trong glossary nên được phép
Hoá đơn điện tử eInvoice Phân biệt với invoice nội bộ

Ví dụ về đơn vị hành chính là lý do glossary đáng làm: khi cấp huyện bị bỏ, những dự án dùng nhất quán province/district/ward chỉ cần tìm đúng một tên để migrate. Những dự án có quanHuyen, district, huyen rải rác thì phải đoán.

Ép quy tắc bằng ESLint naming-convention và refactor an toàn

Quy ước chỉ nằm trong đầu reviewer sẽ trôi dần. @typescript-eslint/naming-convention ép được phần lớn quy tắc ở trên. Rule này cần typed linting cho các selector theo type (như boolean), xem cách bật trong bài thiết lập code quality cho dự án TypeScript.

// eslint.config.mjs (trích phần rules)
import { defineConfig } from 'eslint/config'
import tseslint from 'typescript-eslint'

export default defineConfig({
  files: ['**/*.{ts,tsx}'],
  extends: [tseslint.configs.recommendedTypeChecked],
  languageOptions: {
    parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
  },
  rules: {
    '@typescript-eslint/naming-convention': [
      'error',
      { selector: 'default', format: ['camelCase'], leadingUnderscore: 'allow' },
      // React component và hằng số module
      { selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] },
      { selector: 'function', format: ['camelCase', 'PascalCase'] },
      { selector: 'import', format: ['camelCase', 'PascalCase'] },
      { selector: 'objectLiteralProperty', format: ['camelCase', 'PascalCase', 'UPPER_CASE'] },
      { selector: 'typeLike', format: ['PascalCase'] },
      // Cấm tiền tố I cho interface
      {
        selector: 'interface',
        format: ['PascalCase'],
        custom: { regex: '^I[A-Z]', match: false },
      },
      // Boolean phải có tiền tố (format kiểm tra phần còn lại sau khi bỏ prefix)
      {
        selector: ['variable', 'parameter'],
        types: ['boolean'],
        format: ['PascalCase'],
        prefix: ['is', 'has', 'can', 'should', 'did', 'will'],
      },
      // Biến destructure từ API bên ngoài: không ép
      { selector: 'variable', modifiers: ['destructured'], format: null },
      // Key cần quote ('Content-Type', 'x-request-id'): không ép
      { selector: ['objectLiteralProperty', 'typeProperty'], modifiers: ['requiresQuotes'], format: null },
    ],
  },
})

Ghi chú khi bật rule này:

  • Rule chọn nhóm cụ thể nhất khớp với identifier (selector riêng lẻ ưu tiên hơn selector nhóm, có types/modifiers ưu tiên hơn không có), không phải nhóm viết trước. Đọc phần "Selector ordering" trong docs của rule nếu kết quả khác bạn nghĩ.
  • types: ['boolean'] cần type information. Không bật typed linting thì nhóm đó báo lỗi cấu hình.
  • Dữ liệu đi qua ranh giới (payload API dạng snake_case, cột DB) đừng cố ép. Map sang camelCase ở một lớp duy nhất (DTO mapper hoặc ORM) và để rule bỏ qua chỗ đó.
  • Với codebase đang có, bật ở mức warn trước, xem số lượng, rồi mới chuyển error.

Khi rule báo hàng trăm tên cần đổi, đừng dùng find-and-replace. Dùng "Rename Symbol" của editor (F2 trong VS Code), vì nó đi qua language service của TypeScript và chỉ đổi đúng symbol, kể cả import ở file khác. Với đợt đổi hàng loạt, viết script bằng ts-morph:

// scripts/rename-legacy-names.ts — chạy: npx tsx scripts/rename-legacy-names.ts
import { Project } from 'ts-morph'

const project = new Project({ tsConfigFilePath: 'tsconfig.json' })

const renames: Array<{ file: string; from: string; to: string }> = [
  { file: 'src/domain/order.ts', from: 'IOrder', to: 'Order' },
  { file: 'src/domain/order.ts', from: 'tinhTongTien', to: 'calculateOrderTotalVnd' },
]

for (const { file, from, to } of renames) {
  const sourceFile = project.getSourceFileOrThrow(file)
  const node =
    sourceFile.getInterface(from) ??
    sourceFile.getTypeAlias(from) ??
    sourceFile.getFunction(from) ??
    sourceFile.getVariableDeclaration(from)
  if (!node) throw new Error(`Không tìm thấy ${from} trong ${file}`)
  node.rename(to) // đổi cả mọi chỗ tham chiếu trong project
  console.log(`${file}: ${from} -> ${to}`)
}

project.saveSync()

rename() dùng chính cơ chế rename của language service nên an toàn với import/export và shadowing. Nó không đổi được những gì nằm ngoài type system: key trong JSON gửi lên API, tên cột trong SQL viết tay, string trong test snapshot, biến môi trường. Đó là lý do quy tắc "map ở biên" ở trên quan trọng: đổi tên property của type nội bộ thì an toàn, đổi tên field trong contract với bên ngoài là một migration.

Những lỗi thường gặp

Tên nói dối. getUser() mà bên trong gọi HTTP và ghi cache là vi phạm lời hứa của get. Người gọi sẽ đặt nó trong vòng lặp vì nghĩ nó rẻ.

interface User {
  id: string
}
declare function httpGet(url: string): Promise<User>
const userCache = new Map<string, User>()

// Sai: tên hứa đồng bộ và rẻ
async function getUser(userId: string) {
  const user = await httpGet(`/users/${userId}`)
  userCache.set(userId, user)
  return user
}

// Đúng: tên nói đúng hành vi
async function fetchAndCacheUser(userId: string) {
  const user = await httpGet(`/users/${userId}`)
  userCache.set(userId, user)
  return user
}

Tên theo implementation thay vì ý nghĩa. userArray, orderMap, emailString. Khi đổi Array sang Set, tên trở thành nói dối. Dùng users, orderById, email.

Số không đơn vị. const timeout = 30. Ba mươi giây hay ba mươi mili giây? setTimeout(fn, timeout) sẽ chạy sau 30ms. Viết timeoutMs = 30_000.

Boolean mang nghĩa ba trạng thái. isVerified: boolean | null với null nghĩa là "chưa kiểm tra". Thay bằng verificationStatus: 'unchecked' | 'verified' | 'rejected'.

Đổi tên bằng find-and-replace. Thay order thành purchase bằng regex sẽ đổi luôn orderBy trong query builder và border trong CSS-in-JS. Luôn dùng Rename Symbol hoặc ts-morph.

Glossary chỉ nằm trong Confluence. Glossary không nằm trong repo thì không được review cùng PR thêm khái niệm mới, và sẽ lỗi thời trong vài tháng.

Checklist

  • Grep codebase tìm data, info, temp, result, item ở tầng nghiệp vụ và đổi tên theo domain.
  • Thêm đơn vị vào mọi biến số có đơn vị: Ms, Seconds, Bytes, Grams, Km, Vnd, Percent.
  • Đổi boolean parameter của các hàm public thành options object.
  • Viết bảng quy ước động từ (get/fetch/load/find/compute/build/create) vào CONTRIBUTING hoặc docs của repo.
  • Tạo docs/glossary.md cho thuật ngữ nghiệp vụ tiếng Việt và review nó như code.
  • Thay enum bằng union literal hoặc object as const khi viết code mới.
  • Bật @typescript-eslint/naming-convention ở mức warn, xử lý dần, rồi nâng lên error.
  • Dùng Rename Symbol hoặc ts-morph cho mọi đợt đổi tên, không dùng find-and-replace.

Đọc thêm

Last updated: March 4, 2026

Comments