
Ts Google
- 321 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
ts-google is an agent skill that applies Google's TypeScript style guide with 45 prioritized rules for developers who need consistent, type-safe TypeScript when writing or reviewing code.
About
ts-google is a dot-skills agent skill (v1.1.6, January 2026) encoding Google's TypeScript style guide for AI-assisted writing, review, and refactoring. It organizes 45 rules across 8 categories—from CRITICAL module- and types- prefixes through class, function, control-flow, error-handling, naming, and literal conventions—with per-rule reference markdown files and an AGENTS.md compiled guide. Developers reach for ts-google when generating or reviewing .ts/.tsx files and need enforceable decisions on named exports, import type, no any, interfaces over type aliases, catch unknown, and triple-equals instead of generic lint opinions. Triggers include TypeScript files, type annotations, module imports, class design, and code style decisions.
- ts-google
Ts Google by the numbers
- 321 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,252 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill ts-googleAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 321 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do you enforce Google TypeScript style in AI code?
Use ts-google for development tasks
Who is it for?
TypeScript developers and reviewers who want Google-internal consistency across modules, types, classes, and error handling.
Skip if: Pure JavaScript codebases, Python backends, or teams standardized on a conflicting style guide like Airbnb without adaptation.
When should I use this skill?
User edits or reviews TypeScript files, asks about module imports, type annotations, class design, or Google TS style compliance.
What you get
Refactored TypeScript matching Google rules for modules, types, classes, functions, errors, and naming.
- style-compliant TypeScript code
- refactor guidance per rule
By the numbers
- 45 rules across 8 categories in metadata.json v1.1.6
- Based on Google TypeScript style guide (January 2026)
- Individual reference files per rule under references/
Files
Google TypeScript Best Practices
Comprehensive TypeScript style guide based on Google's internal standards, designed for AI agents and LLMs. Contains 45 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
When to Apply
Reference these guidelines when:
- Writing new TypeScript code
- Organizing modules and imports
- Designing type annotations and interfaces
- Creating classes and functions
- Reviewing code for style consistency
- Refactoring existing TypeScript code
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Module Organization | CRITICAL | module- |
| 2 | Type Safety | CRITICAL | types- |
| 3 | Class Design | HIGH | class- |
| 4 | Function Patterns | HIGH | func- |
| 5 | Control Flow | MEDIUM-HIGH | control- |
| 6 | Error Handling | MEDIUM | error- |
| 7 | Naming & Style | MEDIUM | naming- |
| 8 | Literals & Coercion | LOW-MEDIUM | literal- |
Quick Reference
1. Module Organization (CRITICAL)
- `module-named-exports` - Use named exports over default exports
- `module-no-mutable-exports` - Avoid mutable exports
- `module-es6-modules` - Use ES6 modules exclusively
- `module-no-namespaces` - Avoid TypeScript namespaces
- `module-import-paths` - Use relative paths for project imports
- `module-import-type` - Use import type for type-only imports
- `module-export-api-surface` - Minimize exported API surface
2. Type Safety (CRITICAL)
- `types-no-any` - Never use the any type
- `types-prefer-interfaces` - Prefer interfaces over type aliases for objects
- `types-explicit-structural` - Explicitly annotate structural types
- `types-nullable-patterns` - Handle nullable types correctly
- `types-array-syntax` - Use consistent array type syntax
- `types-no-wrapper-types` - Never use wrapper object types
- `types-prefer-map-set` - Prefer Map and Set over index signatures
- `types-no-empty-object` - Avoid empty object type
3. Class Design (HIGH)
- `class-parameter-properties` - Use parameter properties for constructor assignment
- `class-readonly-properties` - Mark properties readonly when never reassigned
- `class-no-private-fields` - Use TypeScript private over private fields
- `class-no-static-containers` - Avoid container classes with only static members
- `class-constructor-parens` - Always use parentheses in constructor calls
- `class-no-prototype-manipulation` - Never manipulate prototypes directly
4. Function Patterns (HIGH)
- `func-declarations-over-expressions` - Prefer function declarations over expressions
- `func-arrow-concise-bodies` - Use concise arrow function bodies appropriately
- `func-avoid-this-rebinding` - Avoid rebinding this
- `func-rest-parameters` - Use rest parameters over arguments
- `func-generator-syntax` - Use correct generator function syntax
- `func-default-parameters` - Use default parameters sparingly
5. Control Flow (MEDIUM-HIGH)
- `control-always-use-braces` - Always use braces for control structures
- `control-triple-equals` - Always use triple equals
- `control-for-of-iteration` - Prefer for-of over for-in for arrays
- `control-switch-default` - Always include default case in switch
- `control-no-assignment-in-condition` - Avoid assignment in conditional expressions
6. Error Handling (MEDIUM)
- `error-throw-errors` - Always throw Error instances
- `error-catch-unknown` - Type catch clause variables as unknown
- `error-empty-catch-comments` - Document empty catch blocks
- `error-avoid-assertions` - Avoid type and non-null assertions
7. Naming & Style (MEDIUM)
- `naming-identifier-styles` - Use correct identifier naming styles
- `naming-descriptive-names` - Use descriptive names
- `naming-no-decorative-underscores` - Avoid decorative underscores
- `naming-no-interface-prefix` - No I prefix for interfaces
- `naming-constants` - Use CONSTANT_CASE for true constants
8. Literals & Coercion (LOW-MEDIUM)
- `literal-single-quotes` - Use single quotes for strings
- `literal-number-formats` - Use correct number literal formats
- `literal-explicit-coercion` - Use explicit type coercion
- `literal-array-constructor` - Avoid Array constructor
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| AGENTS.md | Complete compiled guide with all rules |
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
TypeScript
Version 0.1.0 Google January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive TypeScript style guide based on Google's internal standards, designed for AI agents and LLMs. Contains 45 rules across 8 categories, prioritized by impact from critical (module organization, type safety) to incremental (literals and coercion). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
Table of Contents
1. Module Organization — CRITICAL
- 1.1 Avoid Mutable Exports — CRITICAL (prevents hard-to-track state mutations)
- 1.2 Avoid TypeScript Namespaces — CRITICAL (prevents runtime overhead and enables tree-shaking)
- 1.3 Minimize Exported API Surface — HIGH (reduces coupling and maintenance burden)
- 1.4 Use ES6 Modules Exclusively — CRITICAL (enables tree-shaking and static analysis)
- 1.5 Use Import Type for Type-Only Imports — HIGH (reduces bundle size by eliminating runtime imports)
- 1.6 Use Named Exports Over Default Exports — CRITICAL (catches import typos at compile time)
- 1.7 Use Relative Paths for Project Imports — HIGH (improves refactoring flexibility and reduces coupling)
2. Type Safety — CRITICAL
- 2.1 Avoid Empty Object Type — HIGH (prevents unexpected type widening)
- 2.2 Explicitly Annotate Structural Types — CRITICAL (catches type mismatches at declaration site)
- 2.3 Handle Nullable Types Correctly — CRITICAL (prevents null reference errors)
- 2.4 Never Use the any Type — CRITICAL (prevents undetected type errors throughout codebase)
- 2.5 Never Use Wrapper Object Types — CRITICAL (prevents type confusion and boxing overhead)
- 2.6 Prefer Interfaces Over Type Aliases for Objects — CRITICAL (better error messages and IDE performance)
- 2.7 Prefer Map and Set Over Index Signatures — HIGH (O(1) operations with proper typing)
- 2.8 Use Consistent Array Type Syntax — HIGH (improves readability and consistency)
3. Class Design — HIGH
- 3.1 Always Use Parentheses in Constructor Calls — MEDIUM (consistent syntax and prevents parsing ambiguity)
- 3.2 Avoid Container Classes with Only Static Members — HIGH (reduces unnecessary abstraction and enables tree-shaking)
- 3.3 Mark Properties Readonly When Never Reassigned — HIGH (prevents accidental mutations and enables optimizations)
- 3.4 Never Manipulate Prototypes Directly — HIGH (prevents VM deoptimization and unpredictable behavior)
- 3.5 Use Parameter Properties for Constructor Assignment — HIGH (reduces boilerplate by 50%)
- 3.6 Use TypeScript Private Over Private Fields — HIGH (consistent access control without runtime overhead)
4. Function Patterns — HIGH
- 4.1 Avoid Rebinding this — HIGH (prevents subtle bugs from this binding issues)
- 4.2 Prefer Function Declarations Over Expressions — HIGH (hoisting enables cleaner code organization)
- 4.3 Use Concise Arrow Function Bodies Appropriately — MEDIUM (improves readability for simple transforms)
- 4.4 Use Correct Generator Function Syntax — MEDIUM (consistent, readable generator definitions)
- 4.5 Use Default Parameters Sparingly — MEDIUM (prevents side effects in parameter defaults)
- 4.6 Use Rest Parameters Over arguments — HIGH (type-safe variadic functions)
5. Control Flow — MEDIUM-HIGH
- 5.1 Always Include Default Case in Switch — MEDIUM (prevents silent failures on unexpected values)
- 5.2 Always Use Braces for Control Structures — MEDIUM-HIGH (prevents bugs from misleading indentation)
- 5.3 Always Use Triple Equals — MEDIUM-HIGH (prevents type coercion bugs)
- 5.4 Avoid Assignment in Conditional Expressions — MEDIUM (prevents accidental assignment bugs)
- 5.5 Prefer for-of Over for-in for Arrays — MEDIUM-HIGH (prevents prototype property enumeration bugs)
6. Error Handling — MEDIUM
- 6.1 Always Throw Error Instances — MEDIUM (provides stack traces for debugging)
- 6.2 Avoid Type and Non-Null Assertions — MEDIUM (prevents hiding type errors)
- 6.3 Document Empty Catch Blocks — MEDIUM (explains intentional error suppression)
- 6.4 Type Catch Clause Variables as Unknown — MEDIUM (enforces safe error handling)
7. Naming & Style — MEDIUM
- 7.1 Avoid Decorative Underscores — MEDIUM (cleaner code without misleading conventions)
- 7.2 No I Prefix for Interfaces — MEDIUM (cleaner type names without Hungarian notation)
- 7.3 Use CONSTANT_CASE for True Constants — MEDIUM (distinguishes immutable values from variables)
- 7.4 Use Correct Identifier Naming Styles — MEDIUM (improves code readability and consistency)
- 7.5 Use Descriptive Names — MEDIUM (improves code maintainability)
8. Literals & Coercion — LOW-MEDIUM
- 8.1 Avoid Array Constructor — LOW-MEDIUM (prevents confusing Array constructor behavior)
- 8.2 Use Correct Number Literal Formats — LOW-MEDIUM (consistent and readable numeric literals)
- 8.3 Use Explicit Type Coercion — LOW-MEDIUM (prevents unexpected coercion behavior)
- 8.4 Use Single Quotes for Strings — LOW-MEDIUM (consistent string syntax throughout codebase)
---
References
1. https://google.github.io/styleguide/tsguide.html 2. https://www.typescriptlang.org/docs/handbook/ 3. https://google.github.io/styleguide/jsguide.html
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Rule Title Here
{1-3 sentences explaining WHY this matters. Focus on the consequences of not following this rule.}
Incorrect ({what's wrong}):
// Bad code example - production-realistic
const example = badPattern()Correct ({what's right}):
// Good code example - minimal diff from incorrect
const example = goodPattern(){Optional sections as needed:}
Alternative ({context}):
// Alternative approach when applicableWhen NOT to use this pattern:
- {Exception 1}
- {Exception 2}
Reference: Google TypeScript Style Guide
{
"version": "1.1.6",
"organization": "Google",
"technology": "TypeScript",
"date": "January 2026",
"abstract": "Comprehensive TypeScript style guide based on Google's internal standards, designed for AI agents and LLMs. Contains 45 rules across 8 categories, prioritized by impact from critical (module organization, type safety) to incremental (literals and coercion). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://google.github.io/styleguide/tsguide.html",
"https://www.typescriptlang.org/docs/handbook/",
"https://google.github.io/styleguide/jsguide.html"
],
"category": "Lang"
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Module Organization (module)
Impact: CRITICAL Description: Import/export patterns affect build times, tree-shaking, and error detection at scale. Named exports catch typos at import time.
2. Type Safety (types)
Impact: CRITICAL Description: Proper typing prevents runtime errors and enables compiler optimizations. Avoiding any is the foundation of type safety.
3. Class Design (class)
Impact: HIGH Description: Class structure affects memory layout, VM optimization, and API surface. Parameter properties reduce boilerplate while maintaining safety.
4. Function Patterns (func)
Impact: HIGH Description: Function design affects call overhead, this binding, and readability. Prefer declarations over expressions for named functions.
5. Control Flow (control)
Impact: MEDIUM-HIGH Description: Proper control flow prevents bugs and improves code predictability. Always use braces and triple equals.
6. Error Handling (error)
Impact: MEDIUM Description: Consistent error handling enables debugging and prevents silent failures. Always throw Error instances with stack traces.
7. Naming & Style (naming)
Impact: MEDIUM Description: Consistent naming improves readability and tooling support. Use descriptive names and follow case conventions.
8. Literals & Coercion (literal)
Impact: LOW-MEDIUM Description: Proper literal usage prevents type coercion bugs. Use explicit coercion functions instead of implicit coercion.
Always Use Parentheses in Constructor Calls
Always use parentheses when calling constructors, even when there are no arguments. This improves consistency and prevents potential parsing issues.
Incorrect (missing parentheses):
const date = new Date
const user = new User
const map = new MapCorrect (with parentheses):
const date = new Date()
const user = new User()
const map = new Map()
const set = new Set<string>()Why it matters:
- Consistent with function call syntax
- Avoids ASI (Automatic Semicolon Insertion) edge cases
- Clearer that construction is happening
- Required for generic type arguments
Reference: Google TypeScript Style Guide - Constructor
Use TypeScript Private Over Private Fields
Use TypeScript's private modifier instead of JavaScript private fields (#field). Private fields have runtime costs and interact poorly with TypeScript features.
Incorrect (JavaScript private fields):
class Counter {
#count = 0 // JavaScript private field
increment() {
this.#count++
}
getCount() {
return this.#count
}
}
// Compiles to WeakMap usage, adds runtime overhead
// Cannot be accessed in tests, even with type assertionsCorrect (TypeScript private modifier):
class Counter {
private count = 0 // TypeScript private
increment() {
this.count++
}
getCount() {
return this.count
}
}
// No runtime overhead, compile-time enforcement
// Can be accessed in tests via type assertions if neededVisibility guidelines:
- Use
privatefor internal implementation details - Use
protectedfor subclass-accessible members - Omit
publicmodifier (it's the default) - Exception:
public readonlyfor parameter properties
Never Manipulate Prototypes Directly
Never modify prototypes directly. It breaks VM optimizations, creates unpredictable behavior, and makes code difficult to understand.
Incorrect (prototype manipulation):
// Extending built-in prototypes
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1)
}
// Dynamic prototype modification
function User(name: string) {
this.name = name
}
User.prototype.greet = function() {
return `Hello, ${this.name}`
}
// Modifying prototype chain
Object.setPrototypeOf(child, parent)Correct (use classes or composition):
// Utility function instead of prototype extension
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// Class-based inheritance
class User {
constructor(public name: string) {}
greet(): string {
return `Hello, ${this.name}`
}
}
// Composition for shared behavior
class UserWithLogging {
constructor(
private user: User,
private logger: Logger
) {}
greet(): string {
this.logger.log('greet called')
return this.user.greet()
}
}Why avoid prototype manipulation:
- Breaks VM hidden class optimizations
- Pollutes global scope
- Creates maintenance nightmares
- Incompatible with strict mode in some cases
Reference: Google TypeScript Style Guide - Modifying prototypes
Avoid Container Classes with Only Static Members
Classes with only static methods add unnecessary indirection. Export functions directly instead for better tree-shaking and simpler code.
Incorrect (static container class):
class StringUtils {
static capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
static truncate(str: string, length: number): string {
return str.length > length ? str.slice(0, length) + '...' : str
}
static isEmpty(str: string): boolean {
return str.trim().length === 0
}
}
// Usage
StringUtils.capitalize('hello')Correct (exported functions):
// string-utils.ts
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function truncate(str: string, length: number): string {
return str.length > length ? str.slice(0, length) + '...' : str
}
export function isEmpty(str: string): boolean {
return str.trim().length === 0
}
// Usage
import { capitalize, truncate } from './string-utils'
capitalize('hello')Benefits:
- Better tree-shaking (unused functions removed)
- No class instantiation overhead
- Simpler imports
- Works with function composition
Use Parameter Properties for Constructor Assignment
Use parameter properties to combine parameter declaration and property assignment into a single declaration, eliminating boilerplate.
Incorrect (manual assignment):
class UserService {
private readonly httpClient: HttpClient
private readonly logger: Logger
private readonly config: Config
constructor(
httpClient: HttpClient,
logger: Logger,
config: Config
) {
this.httpClient = httpClient
this.logger = logger
this.config = config
}
}Correct (parameter properties):
class UserService {
constructor(
private readonly httpClient: HttpClient,
private readonly logger: Logger,
private readonly config: Config
) {}
}Rules for parameter properties:
- Use
private readonlyfor dependencies (most common) - Use
public readonlyfor immutable public properties - Use
protected readonlyfor properties needed by subclasses - Never use
publicwithoutreadonly(exposes mutable state)
Reference: Google TypeScript Style Guide - Parameter properties
Mark Properties Readonly When Never Reassigned
Properties that are never reassigned outside of the constructor should be marked readonly to prevent accidental mutations and communicate intent.
Incorrect (mutable when not needed):
class User {
id: string
name: string
createdAt: Date
constructor(id: string, name: string) {
this.id = id
this.name = name
this.createdAt = new Date()
}
updateName(name: string) {
this.name = name
this.id = 'new-id' // Bug: accidentally mutated id
}
}Correct (readonly for immutable properties):
class User {
readonly id: string
name: string // Only name is mutable
readonly createdAt: Date
constructor(id: string, name: string) {
this.id = id
this.name = name
this.createdAt = new Date()
}
updateName(name: string) {
this.name = name
this.id = 'new-id' // Error: Cannot assign to 'id' because it is read-only
}
}Benefits:
- Compile-time protection against accidental mutation
- Documents immutability intent
- Enables compiler optimizations
- Safer refactoring
Reference: Google TypeScript Style Guide - Field initialization
Always Use Braces for Control Structures
Always use braces for control structures, even when the body is a single statement. This prevents bugs from misleading indentation.
Incorrect (missing braces):
if (condition)
doSomething()
doSomethingElse() // Always executes! Misleading indent
for (const item of items)
process(item)
while (hasMore)
fetchNext()Correct (with braces):
if (condition) {
doSomething()
}
doSomethingElse()
for (const item of items) {
process(item)
}
while (hasMore) {
fetchNext()
}Exception (single-line if):
// Allowed only when entire statement fits on one line
if (isEmpty) return null
if (isReady) start()Why braces matter:
- Prevents Apple's "goto fail" style bugs
- Makes code structure explicit
- Safer when adding statements later
- Consistent with other control structures
Reference: Google TypeScript Style Guide - Control structures
Prefer for-of Over for-in for Arrays
Use for-of loops for arrays and iterables. Use Object.keys(), Object.values(), or Object.entries() for objects. Never use unfiltered for-in.
Incorrect (for-in on array):
const items = ['a', 'b', 'c']
for (const i in items) {
console.log(items[i]) // i is string, enumerates inherited properties
}
// If Array.prototype is extended, this iterates those tooCorrect (for-of for arrays):
const items = ['a', 'b', 'c']
// Direct value access
for (const item of items) {
console.log(item)
}
// When index is needed
for (const [index, item] of items.entries()) {
console.log(index, item)
}Correct (Object methods for objects):
const config = { timeout: 5000, retries: 3 }
// Keys only
for (const key of Object.keys(config)) {
console.log(key)
}
// Values only
for (const value of Object.values(config)) {
console.log(value)
}
// Key-value pairs
for (const [key, value] of Object.entries(config)) {
console.log(key, value)
}If for-in is required, always filter:
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// Safe to use obj[key]
}
}Reference: Google TypeScript Style Guide - Iterating objects
Avoid Assignment in Conditional Expressions
Never use assignment within conditional expressions. It's difficult to distinguish from comparison and leads to bugs.
Incorrect (assignment in condition):
// Easy to mistake for comparison
if (user = getUser()) {
// Is this assignment or typo'd comparison?
}
// Assignment in while condition
while (line = reader.readLine()) {
process(line)
}Correct (separate assignment):
// Clear assignment before condition
const user = getUser()
if (user) {
process(user)
}
// Clear loop structure
let line = reader.readLine()
while (line) {
process(line)
line = reader.readLine()
}
// Or use for-of for iterables
for (const line of reader) {
process(line)
}Why this matters:
=vs==vs===are easy to confuse- Assignment returns the assigned value (truthy/falsy check)
- Code review becomes harder
- Some linters warn/error on this pattern
Reference: Google TypeScript Style Guide - Assignment in conditionals
Always Include Default Case in Switch
All switch statements must include a default case, even if it's empty. All cases must terminate with break, return, or throw.
Incorrect (missing default or fall-through):
function getStatusText(status: number): string {
switch (status) {
case 200:
return 'OK'
case 404:
return 'Not Found'
// Missing default - silent failure on unknown status
}
}
switch (action) {
case 'start':
initialize()
// Missing break - falls through!
case 'stop':
cleanup()
break
}Correct (with default and explicit termination):
function getStatusText(status: number): string {
switch (status) {
case 200:
return 'OK'
case 404:
return 'Not Found'
case 500:
return 'Server Error'
default:
return 'Unknown'
}
}
// Empty default with comment explaining why
switch (knownStatus) {
case Status.Active:
activate()
break
case Status.Inactive:
deactivate()
break
default:
// All cases handled, default unreachable
break
}Empty case fall-through is allowed:
switch (char) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true // All vowels
default:
return false
}Reference: Google TypeScript Style Guide - Switch statements
Always Use Triple Equals
Always use === and !== instead of == and !=. The loose equality operators perform type coercion, leading to unexpected results.
Incorrect (loose equality):
if (value == null) {
// Matches both null and undefined - sometimes intentional
}
if (count == '0') {
// true! Number coerced to string
}
if (arr == false) {
// Empty array is truthy, but this can be true in edge cases
}Correct (strict equality):
if (value === null || value === undefined) {
// Explicit null/undefined check
}
// Or use nullish check when intentional
if (value == null) { // ONLY exception - checking null OR undefined
// Clearly checking for both null and undefined
}
if (count === 0) {
// Type-safe comparison
}
if (arr.length === 0) {
// Explicit empty array check
}The only acceptable use of ==:
// Checking for both null and undefined simultaneously
if (value == null) {
// Equivalent to: value === null || value === undefined
}Avoid Type and Non-Null Assertions
Minimize use of type assertions (as) and non-null assertions (!). They suppress compiler checks and can hide real bugs.
Incorrect (unnecessary assertions):
// Non-null assertion hiding potential bug
const name = user!.name // What if user is null?
// Type assertion without validation
const data = response as UserData // What if response shape is wrong?
// Double assertion (especially dangerous)
const element = unknownValue as unknown as HTMLElementCorrect (runtime checks or proper typing):
// Runtime check instead of assertion
if (!user) {
throw new Error('User is required')
}
const name = user.name // TypeScript knows user is not null
// Type guard for validation
function isUserData(value: unknown): value is UserData {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
)
}
if (!isUserData(response)) {
throw new Error('Invalid user data')
}
const data = response // Properly typed
// Explicit annotation instead of assertion
const config: Config = { timeout: 5000, retries: 3 }When assertions are acceptable:
// With explanatory comment
const element = document.getElementById('app')
// Element exists because we control the HTML
const root = element as HTMLElementType Catch Clause Variables as Unknown
Always type catch clause variables as unknown and narrow before use. This prevents accessing properties that may not exist.
Incorrect (assuming Error type):
try {
await fetchData()
} catch (e) {
// e is implicitly 'any' or 'unknown'
console.log(e.message) // Might not have message property
console.log(e.stack) // Might not have stack property
}Correct (explicit unknown with narrowing):
try {
await fetchData()
} catch (e: unknown) {
// Type guard to safely access Error properties
if (e instanceof Error) {
console.error(e.message)
console.error(e.stack)
} else {
// Handle unexpected throw types
console.error('Unknown error:', String(e))
}
}Helper function for error handling:
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message
}
if (typeof error === 'string') {
return error
}
return 'Unknown error occurred'
}
try {
riskyOperation()
} catch (e: unknown) {
console.error(getErrorMessage(e))
}Reference: Google TypeScript Style Guide - Exception handling
Document Empty Catch Blocks
Empty catch blocks are allowed only with comments explaining why the error is intentionally suppressed.
Incorrect (unexplained empty catch):
try {
parseJSON(input)
} catch (e) {
// Silent failure - why?
}
try {
await deleteFile(path)
} catch {
// What errors are we ignoring?
}Correct (documented empty catch):
try {
cachedValue = parseJSON(localStorage.getItem('cache'))
} catch (e: unknown) {
// Cache may be corrupted or missing; continue with empty cache
}
try {
await deleteFile(tempPath)
} catch (e: unknown) {
// File may already be deleted; safe to ignore
}
// Alternative: explicit fallback
let config: Config
try {
config = parseConfig(rawInput)
} catch (e: unknown) {
// Invalid config format; use defaults
config = DEFAULT_CONFIG
}When empty catch is appropriate:
- Optional cleanup operations
- Cache operations that can fail silently
- Fallback to default behavior
- Operations where failure is expected and handled elsewhere
When NOT to use empty catch:
- Critical operations
- User-facing errors
- Debugging/development
Reference: Google TypeScript Style Guide - Empty catch blocks
Always Throw Error Instances
Always throw Error or Error subclass instances. Never throw strings, objects, or other primitives. Error instances provide stack traces.
Incorrect (non-Error throws):
// String - no stack trace
throw 'Something went wrong'
// Object - no stack trace
throw { message: 'Failed', code: 500 }
// Number - no context
throw 404Correct (Error instances):
// Standard Error
throw new Error('Something went wrong')
// Built-in error types
throw new TypeError('Expected string, got number')
throw new RangeError('Index out of bounds')
// Custom error class
class ValidationError extends Error {
constructor(
message: string,
public readonly field: string
) {
super(message)
this.name = 'ValidationError'
}
}
throw new ValidationError('Invalid email format', 'email')Catching unknown errors:
try {
riskyOperation()
} catch (e: unknown) {
// Always catch as unknown
if (e instanceof Error) {
console.error(e.message, e.stack)
} else {
// Handle non-Error throws from third-party code
throw new Error(`Unexpected error: ${String(e)}`)
}
}Reference: Google TypeScript Style Guide - Exceptions
Use Concise Arrow Function Bodies Appropriately
Use concise arrow function bodies (without braces) only when the return value is used. Use block bodies when the return value is ignored or when multiple statements are needed.
Incorrect (mismatched body style):
// Block body when concise would work
const doubled = numbers.map(n => {
return n * 2
})
// Concise body when return value is ignored
button.addEventListener('click', e => console.log(e))
// Return value of console.log is ignored but expression returns itCorrect (appropriate body style):
// Concise body when return value is used
const doubled = numbers.map(n => n * 2)
const names = users.map(user => user.name)
const filtered = items.filter(item => item.active)
// Block body when return value is ignored
button.addEventListener('click', (e) => {
console.log(e)
})
// Block body for multiple statements
const processed = items.map((item) => {
const normalized = normalize(item)
return transform(normalized)
})Using void operator to clarify intent:
// Explicitly discard return value with void
myPromise.then(v => void console.log(v))
// Makes it clear return value is intentionally ignoredReference: Google TypeScript Style Guide - Arrow function bodies
Avoid Rebinding this
Never use function() expressions that access this. Never rebind this unnecessarily. Use arrow functions or explicit parameters instead.
Incorrect (this binding issues):
class Counter {
count = 0
// Function expression loses this context
increment() {
setTimeout(function() {
this.count++ // this is undefined or wrong
}, 1000)
}
// Unnecessary bind
setupHandler() {
button.addEventListener('click', this.handleClick.bind(this))
}
}Correct (proper this handling):
class Counter {
count = 0
// Arrow function preserves this
increment() {
setTimeout(() => {
this.count++ // this is correctly bound
}, 1000)
}
// Arrow property for event handlers
handleClick = () => {
this.count++
}
setupHandler() {
button.addEventListener('click', this.handleClick)
}
}Alternative (explicit parameter):
// Pass context explicitly instead of relying on this
function processUser(user: User, logger: Logger) {
logger.log(user.name)
}
// Instead of
class UserProcessor {
process() {
this.logger.log(this.user.name) // Depends on this binding
}
}Reference: Google TypeScript Style Guide - this
Prefer Function Declarations Over Expressions
Use function declarations for named functions. They are hoisted, making code organization more flexible, and provide better stack traces.
Incorrect (function expression):
// Arrow function stored in const
const calculateTotal = (items: Item[]): number => {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Anonymous function expression
const formatDate = function(date: Date): string {
return date.toISOString()
}Correct (function declaration):
// Function declaration - hoisted, better stack traces
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
function formatDate(date: Date): string {
return date.toISOString()
}When to use arrow functions:
- Callbacks:
items.map(item => item.price) - When explicit typing is needed:
const handler: EventHandler = (e) => {} - Preserving
thiscontext
When to use function expressions:
- Conditional function assignment
- Functions passed directly to other functions
Reference: Google TypeScript Style Guide - Function declarations
Use Default Parameters Sparingly
Default parameter initializers should be simple values. Avoid side effects, complex expressions, or mutable default values.
Incorrect (complex or side-effect defaults):
// Side effect in default
function createUser(name: string, id = generateId()) {
// generateId() called even when id is provided as undefined
}
// Mutable default object
function processConfig(config = { timeout: 5000 }) {
config.timeout = 10000 // Mutates default object
}
// Complex expression
function calculate(
value: number,
multiplier = getGlobalMultiplier() * localFactor
) {}Correct (simple defaults):
// Simple literal defaults
function createUser(name: string, id?: string) {
const userId = id ?? generateId() // Explicit generation
}
// Spread to avoid mutation
function processConfig(config: Partial<Config> = {}) {
const fullConfig = { timeout: 5000, ...config }
}
// Simple defaults only
function greet(name: string, greeting = 'Hello') {
return `${greeting}, ${name}`
}
// Optional parameter with explicit handling
function fetchData(url: string, timeout?: number) {
const actualTimeout = timeout ?? DEFAULT_TIMEOUT
}Guidelines:
- Use literals, constants, or simple references
- Avoid function calls in defaults
- Never mutate default values
- Consider optional parameters with explicit handling
Reference: Google TypeScript Style Guide - Default parameters
Use Correct Generator Function Syntax
Attach the * to the function keyword with no space. For yield*, attach to the yield keyword. This provides visual consistency.
Incorrect (inconsistent asterisk placement):
// Space before asterisk
function * generator() {
yield 1
}
// Asterisk attached to name
function *generator() {
yield 1
}
// Inconsistent yield* spacing
function* delegate() {
yield * otherGenerator()
}Correct (asterisk on keyword):
// Generator function
function* numberGenerator(): Generator<number> {
yield 1
yield 2
yield 3
}
// Delegating generator
function* combined(): Generator<number> {
yield* numberGenerator()
yield* [4, 5, 6]
}
// Generator method in class
class DataStream {
*[Symbol.iterator](): Generator<Data> {
for (const item of this.items) {
yield item
}
}
}
// Async generator
async function* fetchPages(): AsyncGenerator<Page> {
let page = 1
while (true) {
const data = await fetchPage(page++)
if (!data) break
yield data
}
}Reference: Google TypeScript Style Guide - Generator functions
Use Rest Parameters Over arguments
Use rest parameters (...args) instead of the arguments object. Rest parameters are typed, work with arrow functions, and are more intuitive.
Incorrect (arguments object):
function sum() {
let total = 0
for (let i = 0; i < arguments.length; i++) {
total += arguments[i] // No type checking
}
return total
}
// arguments doesn't work in arrow functions
const multiply = () => {
return Array.from(arguments).reduce((a, b) => a * b, 1)
// Error: 'arguments' is not defined
}Correct (rest parameters):
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0)
}
// Works with arrow functions
const multiply = (...numbers: number[]): number => {
return numbers.reduce((a, b) => a * b, 1)
}
// Typed variadic function
function log(level: string, ...messages: unknown[]): void {
console.log(`[${level}]`, ...messages)
}Calling variadic functions with spread:
const values = [1, 2, 3, 4, 5]
const total = sum(...values) // Spread array into argumentsNever:
- Name any parameter
arguments - Use
Function.prototype.apply()for variadic calls
Avoid Array Constructor
Never use the Array() constructor. Its behavior is confusing (single number creates sparse array). Use array literals or Array.from().
Incorrect (Array constructor):
// Single number creates sparse array of that length
const arr = new Array(3) // [empty × 3], not [3]
// Multiple arguments create array with those elements
const arr2 = new Array(1, 2, 3) // [1, 2, 3]
// Inconsistent behavior is confusing
const a = Array(3) // [empty × 3]
const b = Array('3') // ['3']Correct (array literals and Array.from):
// Array literals
const empty: number[] = []
const numbers = [1, 2, 3]
const strings = ['a', 'b', 'c']
// Array.from for creating arrays with specific length
const fiveZeros = Array.from({ length: 5 }, () => 0) // [0, 0, 0, 0, 0]
const indices = Array.from({ length: 5 }, (_, i) => i) // [0, 1, 2, 3, 4]
// Array.from with typed generics
const typed = Array.from<number>({ length: 3 }) // [undefined, undefined, undefined]
// Spread for copying
const copy = [...original]
// fill() for same value
const threes = new Array(5).fill(3) // [3, 3, 3, 3, 3] - fill() makes it denseObject constructor also forbidden:
// Incorrect
const obj = new Object()
const obj2 = Object()
// Correct
const obj = {}
const obj2: Record<string, unknown> = {}Reference: Google TypeScript Style Guide - Array constructor
Use Explicit Type Coercion
Use explicit coercion functions (String(), Number(), Boolean()) instead of implicit coercion or unary operators.
Incorrect (implicit coercion):
// Unary + for number coercion
const num = +inputString
// String concatenation for coercion
const str = '' + value
// Double negation for boolean
const bool = !!value
// parseInt without validation
const parsed = parseInt(input)Correct (explicit coercion):
// Explicit String coercion
const str = String(value)
// Explicit Number coercion with validation
const num = Number(inputString)
if (!Number.isFinite(num)) {
throw new Error('Invalid number')
}
// Explicit Boolean coercion
const bool = Boolean(value)
// Template literal for string conversion
const message = `Value: ${value}`Implicit coercion allowed in conditionals:
// Truthy/falsy checks are acceptable
if (array.length) {
// Non-empty array
}
if (str) {
// Non-empty string
}
// Exception: enums require explicit comparison
enum Status {
NONE = 0,
ACTIVE = 1,
}
// Incorrect - implicit coercion of enum
if (status) {} // NONE (0) is falsy!
// Correct - explicit comparison
if (status !== Status.NONE) {}Use Correct Number Literal Formats
Use lowercase prefixes for non-decimal numbers. Never use leading zeros for decimal numbers. Use underscores for readability in long numbers.
Incorrect (inconsistent or hard-to-read formats):
// Uppercase prefix
const hex = 0XABC
// Leading zero (looks like octal in some languages)
const port = 0080
// Hard to read large numbers
const billion = 1000000000Correct (consistent lowercase prefixes):
// Hexadecimal - lowercase 0x
const hexColor = 0xffffff
const permissions = 0x755
// Binary - lowercase 0b
const flags = 0b1010
const mask = 0b11110000
// Octal - lowercase 0o
const fileMode = 0o755
// Decimal - no leading zeros
const port = 80
const count = 42
// Underscores for readability (ES2021+)
const billion = 1_000_000_000
const bytes = 0xff_ff_ff_ff
const binary = 0b1111_0000_1111_0000Numeric parsing:
// Use Number() for parsing
const parsed = Number(input)
if (!Number.isFinite(parsed)) {
throw new Error('Invalid number')
}
// Never use parseInt without radix (except radix 10)
const decimal = Number(str) // Preferred
const hex = parseInt(hexStr, 16) // When radix neededUse Single Quotes for Strings
Use single quotes for ordinary string literals. Use template literals for strings that contain interpolation or span multiple lines.
Incorrect (double quotes and concatenation):
// Double quotes for ordinary strings
const name = "Alice"
const message = "Hello, world"
// String concatenation instead of template
const greeting = "Hello, " + name + "!"
// Line continuation with backslash
const longString = "This is a very long \
string that continues"Correct (single quotes and template literals):
// Single quotes for ordinary strings
const name = 'Alice'
const message = 'Hello, world'
// Template literal for interpolation
const greeting = `Hello, ${name}!`
// Template literal for multi-line
const longString = `
This is a very long
string that spans
multiple lines
`
// Single quotes with escaping when needed
const quote = 'She said, "Hello"'
const apostrophe = "It's working" // Double quotes to avoid escapingWhen to use template literals:
- String interpolation:
\Hello, ${name}\`` - Multi-line strings
- Complex string building
Use ES6 Modules Exclusively
ES6 modules enable static analysis, tree-shaking, and consistent behavior across environments. Never use legacy module systems.
Incorrect (legacy patterns):
// CommonJS - no static analysis possible
const fs = require('fs')
// TypeScript namespaces - creates runtime overhead
namespace MyApp {
export class User {}
}
// Triple-slash references - fragile path resolution
/// <reference path="./types.d.ts" />Correct (ES6 modules):
// Named imports
import { readFile, writeFile } from 'fs'
// Namespace imports for large APIs
import * as fs from 'fs'
// Side-effect imports (use sparingly)
import './polyfills'When to use each import style:
- Named imports: accessing few symbols frequently
- Namespace imports: accessing many symbols from large APIs
- Side-effect imports: libraries requiring initialization
Reference: Google TypeScript Style Guide - Imports
Minimize Exported API Surface
Export only what consumers need. Internal implementation details should remain private to allow refactoring without breaking changes.
Incorrect (over-exporting):
// user-service.ts
export const API_ENDPOINT = '/api/users'
export const MAX_RETRIES = 3
export function validateUser(user: User): boolean {
return user.name.length > 0
}
export function formatUserForApi(user: User): ApiUser {
return { userName: user.name, userId: user.id }
}
export async function createUser(name: string): Promise<User> {
const user = { name, id: generateId() }
if (!validateUser(user)) throw new Error('Invalid')
const apiUser = formatUserForApi(user)
return sendToApi(apiUser)
}Correct (minimal exports):
// user-service.ts
const API_ENDPOINT = '/api/users'
const MAX_RETRIES = 3
function validateUser(user: User): boolean {
return user.name.length > 0
}
function formatUserForApi(user: User): ApiUser {
return { userName: user.name, userId: user.id }
}
// Only export the public API
export async function createUser(name: string): Promise<User> {
const user = { name, id: generateId() }
if (!validateUser(user)) throw new Error('Invalid')
const apiUser = formatUserForApi(user)
return sendToApi(apiUser)
}Benefits:
- Internal functions can be refactored freely
- Smaller public API is easier to document
- Clearer boundary between public and private code
Reference: Google TypeScript Style Guide - Export visibility
Use Relative Paths for Project Imports
Use relative paths (./foo) for imports within your project to maintain flexibility when moving code between directories.
Incorrect (absolute or alias paths for local code):
// Tightly coupled to project structure
import { User } from '@app/models/user'
import { createUser } from 'src/services/user-service'Correct (relative paths):
// Flexible, works when files are moved together
import { User } from './models/user'
import { createUser } from '../services/user-service'When to use non-relative imports:
- External npm packages:
import { useState } from 'react' - Configured path aliases for truly shared code
- Generated code or type definitions
Benefits:
- Files can be moved together without breaking imports
- No build configuration required
- Clear dependency direction visible in path
Use Import Type for Type-Only Imports
When importing types that are only used for type annotations (not at runtime), use import type to ensure they're removed during compilation.
Incorrect (regular import for types):
import { User, UserService } from './user'
// User is only used as type, UserService is used at runtime
function getUser(service: UserService, id: string): User {
return service.get(id)
}
// 'User' import may remain in bundle depending on transpilerCorrect (explicit type import):
import type { User } from './user'
import { UserService } from './user'
function getUser(service: UserService, id: string): User {
return service.get(id)
}
// 'User' guaranteed to be removed from bundleAlternative (inline type modifier):
import { type User, UserService } from './user'Benefits:
- Guaranteed removal of type-only imports
- Clearer intent in code review
- Prevents accidental runtime usage of types
Reference: TypeScript 3.8 - Type-Only Imports
Use Named Exports Over Default Exports
Named exports error when import statements try to import something that hasn't been declared, catching typos and refactoring mistakes at compile time.
Incorrect (default export allows any import name):
// user.ts
export default class User {
constructor(public name: string) {}
}
// main.ts
import Usr from './user' // Typo not caught - silently worksCorrect (named export catches typos):
// user.ts
export class User {
constructor(public name: string) {}
}
// main.ts
import { Usr } from './user' // Error: Module has no exported member 'Usr'
import { User } from './user' // CorrectBenefits:
- Compile-time error detection for typos
- Better tree-shaking in bundlers
- Consistent import names across codebase
- Easier refactoring with IDE support
Reference: Google TypeScript Style Guide - Export visibility
Avoid Mutable Exports
Mutable exports create hidden state that can be modified from anywhere, making code behavior unpredictable and bugs difficult to trace.
Incorrect (mutable export):
// config.ts
export let currentUser: User | null = null
export let apiEndpoint = 'https://api.example.com'
// somewhere.ts
import { currentUser, apiEndpoint } from './config'
apiEndpoint = 'https://staging.example.com' // Mutates global stateCorrect (immutable exports with explicit setters):
// config.ts
let _currentUser: User | null = null
const _apiEndpoint = 'https://api.example.com'
export function getCurrentUser(): User | null {
return _currentUser
}
export function setCurrentUser(user: User | null): void {
_currentUser = user
}
export const apiEndpoint = _apiEndpoint // const exportAlternative (readonly object):
export const config = {
apiEndpoint: 'https://api.example.com',
timeout: 5000,
} as constAvoid TypeScript Namespaces
TypeScript namespaces create runtime objects that prevent tree-shaking and add unnecessary overhead. Use ES6 modules for code organization.
Incorrect (TypeScript namespace):
namespace MyApp {
export interface User {
name: string
}
export function createUser(name: string): User {
return { name }
}
}
// Usage
const user = MyApp.createUser('Alice')
// Compiles to runtime object with all exports bundledCorrect (ES6 modules):
// user.ts
export interface User {
name: string
}
export function createUser(name: string): User {
return { name }
}
// main.ts
import { createUser } from './user'
const user = createUser('Alice')
// Tree-shakeable, no runtime overheadException: Namespaces may be required when interfacing with external third-party code that uses them.
Reference: Google TypeScript Style Guide - Namespaces vs Modules
Use CONSTANT_CASE for True Constants
Use CONSTANT_CASE only for deeply immutable values at module scope or as static readonly class properties. Local constants use lowerCamelCase.
Incorrect (wrong case for scope):
// Local variable shouldn't be CONSTANT_CASE
function calculate() {
const MAX_VALUE = 100 // This is a local const
return MAX_VALUE * 2
}
// Mutable object in CONSTANT_CASE
const DEFAULT_CONFIG = {
timeout: 5000,
retries: 3,
}
DEFAULT_CONFIG.timeout = 10000 // Mutated!Correct (appropriate case):
// Module-level true constants
const MAX_RETRY_COUNT = 3
const API_BASE_URL = 'https://api.example.com'
const HTTP_STATUS_OK = 200
// Immutable object constant
const DEFAULT_CONFIG = {
timeout: 5000,
retries: 3,
} as const // Truly immutable
// Local constants use camelCase
function calculate() {
const maxValue = 100
return maxValue * 2
}
// Class static readonly
class HttpClient {
static readonly DEFAULT_TIMEOUT = 5000
static readonly MAX_RETRIES = 3
}CONSTANT_CASE requirements:
- Module-level or static readonly
- Deeply immutable (primitives or
as const) - Never reassigned
- Represents a true constant value, not just a
constbinding
Reference: Google TypeScript Style Guide - Constants
Use Descriptive Names
Use descriptive names that clearly communicate purpose. Avoid ambiguous abbreviations. Short names are acceptable only in very limited scopes.
Incorrect (ambiguous or abbreviated):
// Unclear abbreviations
const usr = getUser()
const cfg = loadConfig()
const btn = document.querySelector('button')
// Single letters in wide scope
function processData(d: Data) {
const r = transform(d)
return format(r)
}
// Meaningless names
const temp = calculateValue()
const data = fetchData() // What kind of data?Correct (descriptive):
// Clear, full words
const currentUser = getUser()
const appConfig = loadConfig()
const submitButton = document.querySelector('button')
// Descriptive names
function processUserData(userData: UserData) {
const transformedData = transform(userData)
return format(transformedData)
}
// Specific names
const discountedPrice = calculateDiscountedPrice()
const userPreferences = fetchUserPreferences()When short names are acceptable:
// Very limited scope (≤10 lines)
users.map(u => u.name)
items.filter(x => x.active)
// Conventional loop variables
for (let i = 0; i < count; i++) {}
// Mathematical/domain conventions
const x = point.x
const y = point.yReference: Google TypeScript Style Guide - Descriptive names
Use Correct Identifier Naming Styles
Follow consistent naming conventions based on identifier type. This improves readability and makes code intent clear.
Naming conventions:
| Style | Usage |
|---|---|
UpperCamelCase | Classes, interfaces, types, enums, decorators, type parameters |
lowerCamelCase | Variables, parameters, functions, methods, properties, module aliases |
CONSTANT_CASE | Global constants, enum values, static readonly properties |
Incorrect (wrong case for identifier type):
// Wrong case for type
interface user_data {
user_name: string
}
// Wrong case for constant
const maxRetries = 3
// Wrong case for class
class userService {}
// Leading underscore for "private"
const _internalValue = 42Correct (proper case by identifier type):
// Interface - UpperCamelCase
interface UserData {
userName: string
}
// Global constant - CONSTANT_CASE
const MAX_RETRIES = 3
// Class - UpperCamelCase
class UserService {}
// Variable - lowerCamelCase
const internalValue = 42
// Enum - UpperCamelCase with CONSTANT_CASE values
enum HttpStatus {
OK = 200,
NOT_FOUND = 404,
INTERNAL_ERROR = 500,
}Treat acronyms as words:
// Correct
loadHttpUrl()
parseXmlDocument()
class HtmlParser {}
// Incorrect
loadHTTPURL()
parseXMLDocument()
class HTMLParser {}Reference: Google TypeScript Style Guide - Naming style
Avoid Decorative Underscores
Never use leading or trailing underscores for identifiers. Use TypeScript's private modifier for private members instead.
Incorrect (decorative underscores):
class UserService {
_users: User[] = [] // Leading underscore for "private"
__internalState = {} // Double underscore
users_ = [] // Trailing underscore
_loadUsers() {
// Leading underscore for "private" method
}
}
// Underscore prefix for unused variables
function process(_unused: string, value: number) {
return value * 2
}Correct (TypeScript modifiers):
class UserService {
private users: User[] = []
private internalState = {}
private loadUsers() {
// Truly private with TypeScript
}
}
// Omit unused parameters or use explicit void
function process(value: number) {
return value * 2
}
// Or use void for required unused params
function callback(_event: Event) {
// Parameter required by signature but unused
void _event // Explicit acknowledgment
}Exception - external API requirements:
// Some external libraries require specific naming
interface WindowWithGlobals extends Window {
__REDUX_DEVTOOLS_EXTENSION__?: DevToolsExtension
}Reference: Google TypeScript Style Guide - Naming conventions
No I Prefix for Interfaces
Never prefix interface names with I or suffix with Interface. TypeScript's structural typing makes these markers unnecessary.
Incorrect (Hungarian notation):
interface IUser {
name: string
email: string
}
interface IUserService {
getUser(id: string): IUser
}
interface UserInterface {
name: string
}
// Leads to awkward usage
function processUser(user: IUser): void {}Correct (clean names):
interface User {
name: string
email: string
}
interface UserService {
getUser(id: string): User
}
// Clean usage
function processUser(user: User): void {}
// Class implementing interface
class DefaultUserService implements UserService {
getUser(id: string): User {
return { name: 'Alice', email: 'alice@example.com' }
}
}Why avoid prefixes:
- TypeScript uses structural typing, not nominal
- Interfaces and types are interchangeable in many contexts
- Prefixes add noise without value
- Modern IDEs show type information on hover
Reference: Google TypeScript Style Guide - Naming conventions
Use Consistent Array Type Syntax
Use T[] for simple types and Array<T> for complex types (unions, objects). This improves readability and prevents parsing ambiguity.
Incorrect (inconsistent or complex syntax):
// Generic for simple types
const numbers: Array<number> = [1, 2, 3]
const names: Array<string> = ['Alice', 'Bob']
// Bracket syntax for complex types (hard to read)
const items: { id: number; name: string }[] = []
const mixed: (string | number)[] = []Correct (appropriate syntax):
// Bracket syntax for simple types
const numbers: number[] = [1, 2, 3]
const names: string[] = ['Alice', 'Bob']
const matrix: number[][] = [[1, 2], [3, 4]]
// Generic syntax for complex types
const items: Array<{ id: number; name: string }> = []
const mixed: Array<string | number> = []
const callbacks: Array<(value: number) => void> = []
// Readonly arrays
const constants: readonly number[] = [1, 2, 3]Summary:
- Simple types:
T[],readonly T[] - Multi-dimensional:
T[][] - Complex/union types:
Array<T> - Tuples:
[T, U]
Reference: Google TypeScript Style Guide - Array type
Explicitly Annotate Structural Types
Always explicitly declare structural types for objects. This catches field mismatches at the declaration site rather than at usage.
Incorrect (inferred type):
interface User {
name: string
email: string
}
// Type is inferred, typo not caught here
const user = {
name: 'Alice',
emial: 'alice@example.com', // Typo!
}
function sendEmail(user: User) {
console.log(user.email)
}
sendEmail(user) // Error here, far from sourceCorrect (explicit annotation):
interface User {
name: string
email: string
}
// Error caught immediately at declaration
const user: User = {
name: 'Alice',
emial: 'alice@example.com', // Error: 'emial' does not exist in type 'User'
}Alternative (satisfies for inference with checking):
const user = {
name: 'Alice',
email: 'alice@example.com',
} satisfies User
// Type is inferred but validated against UserBenefits:
- Errors appear at the source, not at usage
- Self-documenting code
- Better refactoring support
Never Use the any Type
The any type allows assignment into any other type and dereferencing any property, completely disabling type checking and enabling undetected errors.
Incorrect (using any):
function processData(data: any) {
return data.items.map((item: any) => item.value)
// No type checking - typos, wrong properties, all pass silently
}
const result = processData({ itms: [] }) // Typo not caughtCorrect (use specific types or unknown):
interface DataPayload {
items: Array<{ value: number }>
}
function processData(data: DataPayload) {
return data.items.map((item) => item.value)
// Full type checking
}
const result = processData({ itms: [] }) // Error: 'itms' not in DataPayloadAlternative (use unknown for truly unknown values):
function processUnknown(data: unknown) {
// Must narrow type before use
if (typeof data === 'object' && data !== null && 'items' in data) {
// Safe to access data.items
}
}When you think you need any: 1. Define an interface for the expected shape 2. Use unknown with type narrowing 3. Use generics for flexible typing
Reference: Google TypeScript Style Guide - Any type
Avoid Empty Object Type
Never use {} as a type. It matches almost everything except null and undefined, which is almost never the intended behavior.
Incorrect (empty object type):
// Matches strings, numbers, arrays - anything non-nullish
function process(value: {}) {
// No useful operations available
}
process('string') // Allowed!
process(123) // Allowed!
process([1, 2, 3]) // Allowed!Correct (use appropriate types):
// For any value including null/undefined
function processAnything(value: unknown) {
// Must narrow type before use
}
// For non-null objects only
function processObject(value: object) {
// Excludes primitives
}
// For dictionaries with known value type
function processDictionary(value: Record<string, unknown>) {
// Can iterate over properties
}
// For specific shape
interface Config {
timeout: number
retries: number
}
function processConfig(value: Config) {
// Full type safety
}Type comparison:
{}- Everything exceptnull/undefinedobject- Non-primitive values onlyunknown- Everything, requires narrowingRecord<K, V>- Dictionary with typed values
Reference: Google TypeScript Style Guide - {} type
Never Use Wrapper Object Types
Never use wrapper types (String, Boolean, Number, Symbol, BigInt). Use lowercase primitive types. Never instantiate wrappers with new.
Incorrect (wrapper types):
// Wrapper types as annotations
function greet(name: String): Boolean {
return name.length > 0
}
// Instantiating wrapper objects
const message = new String('hello')
const count = new Number(42)
const flag = new Boolean(true)
// These create objects, not primitives!
typeof message // 'object', not 'string'Correct (primitive types):
// Primitive type annotations
function greet(name: string): boolean {
return name.length > 0
}
// Literal values
const message = 'hello'
const count = 42
const flag = true
// Coercion without new
const str = String(someValue)
const num = Number(someValue)
const bool = Boolean(someValue)Why this matters:
String !== stringin TypeScript- Wrapper objects have different identity semantics
- Unnecessary memory allocation
- Confusing behavior in comparisons
Handle Nullable Types Correctly
Type aliases must NOT include |null or |undefined. Add nullability only at usage sites. Prefer optional properties over |undefined.
Incorrect (nullability in type alias):
// Nullability baked into type
type CoffeeResponse = Latte | Americano | undefined
interface UserCache {
user: User | null // Forces all consumers to handle null
}Correct (nullability at usage site):
// Clean base type
type CoffeeResponse = Latte | Americano
// Nullability added where needed
class CoffeeService {
getOrder(): CoffeeResponse | undefined {
// May not find an order
}
}
interface UserCache {
user?: User // Optional property preferred
}Guidelines:
- Use
undefinedfor JavaScript APIs (more idiomatic) - Use
nullfor DOM and Google APIs (conventional) - Prefer
field?: Typeoverfield: Type | undefined - Check for both with
value == nullwhen appropriate
Incorrect (redundant undefined):
interface Config {
timeout: number | undefined // Redundant
}Correct (optional property):
interface Config {
timeout?: number // Cleaner, same semantics
}Reference: Google TypeScript Style Guide - Null vs Undefined
Prefer Interfaces Over Type Aliases for Objects
Interfaces provide better error messages (at declaration vs usage), better IDE support, and clearer semantics for object shapes.
Incorrect (type alias for object):
type User = {
firstName: string
lastName: string
email: string
}
type UserWithId = User & {
id: string
}Correct (interface):
interface User {
firstName: string
lastName: string
email: string
}
interface UserWithId extends User {
id: string
}When to use type aliases:
- Union types:
type Status = 'pending' | 'active' | 'inactive' - Mapped types:
type Readonly<T> = { readonly [K in keyof T]: T[K] } - Tuple types:
type Point = [number, number] - Function types:
type Handler = (event: Event) => void
Benefits of interfaces:
- Declaration merging for extending third-party types
- Better error locality (errors at interface, not usage)
- More intuitive
extendsvs&for inheritance - Better TypeScript compiler performance
Reference: Google TypeScript Style Guide - Interfaces vs Type Aliases
Prefer Map and Set Over Index Signatures
Use Map and Set instead of objects with index signatures for better type safety, predictable iteration order, and O(1) operations.
Incorrect (index signature objects):
// Loose typing, prototype pollution risk
const userScores: { [key: string]: number } = {}
userScores['alice'] = 100
userScores['bob'] = 85
// Checking existence is awkward
if (userScores['charlie'] !== undefined) {
// ...
}
// toString, hasOwnProperty are valid keys (prototype issues)Correct (Map/Set):
// Type-safe, no prototype pollution
const userScores = new Map<string, number>()
userScores.set('alice', 100)
userScores.set('bob', 85)
// Clear existence check
if (userScores.has('charlie')) {
const score = userScores.get('charlie')!
}
// For unique values
const activeUsers = new Set<string>()
activeUsers.add('alice')
activeUsers.add('bob')When to use index signatures:
- JSON serialization (Map doesn't serialize cleanly)
- Known, finite set of keys: use Record type instead
type UserRole = 'admin' | 'user' | 'guest'
const permissions: Record<UserRole, string[]> = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read'],
}Related skills
How it compares
Pick ts-google over generic ESLint prompts when you need opinionated Google TS conventions with per-rule examples for agent-driven refactors.
FAQ
How many rules does ts-google include?
ts-google bundles 45 rules across 8 categories in metadata.json v1.1.6. Categories span module organization, type safety, class design, functions, control flow, errors, naming, and literals.
Should ts-google projects use default exports?
ts-google rule module-named-exports requires named exports over default exports. The guide also discourages mutable exports, TypeScript namespaces, and type-only imports without import type.