Đặ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.
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,orderlà đủ. - 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:
...Atcho một thời điểm tuyệt đối (instant, có timezone):createdAt,paidAt,expiresAt. Lưu UTC, kiểuDatehoặc ISO string cóZ....Datehoặc...Oncho ngày lịch không có giờ:birthDate,dueDate,deliveredOn. Kiểu stringYYYY-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àinterfacehaytype, 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 rulenaming-conventioncó sẵn ví dụ cấm tiền tố này. - Union literal thay enum.
enumsinh code JavaScript thật (một object), không phải "erasable syntax". FlagerasableSyntaxOnly(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, patternas 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ẳnT, 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:
- Không dùng identifier có dấu. JavaScript cho phép (
const tổngTiền = 0là 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ềntrô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ó. - 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. - 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,citycùng chỉ một khái niệm trong ba module. - Không trộn trong cùng một tên:
tongAmount,getDonHanglà 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
warntrước, xem số lượng, rồi mới chuyểnerror.
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.mdcho thuật ngữ nghiệp vụ tiếng Việt và review nó như code. - Thay
enumbằng union literal hoặc objectas constkhi viết code mới. - Bật
@typescript-eslint/naming-conventionở mứcwarn, xử lý dần, rồi nâng lênerror. - Dùng Rename Symbol hoặc ts-morph cho mọi đợt đổi tên, không dùng find-and-replace.