name: google-typescript-style description: >- Use when writing or reviewing TypeScript under Google's source, type-system, naming, module, documentation, and conformance conventions. Do not use for plain JavaScript; use google-javascript-style instead.
Google TypeScript Style Guide
Apply this guidance to the actual project. Repository requirements and newer authoritative guidance take precedence.
Import paths
TypeScript code must use paths to import other TypeScript code. Paths may be
relative, i.e. starting with . or ..,
or rooted at the base directory, e.g.
root/path/to/file.
Code should use relative imports (./foo) rather than absolute imports
path/to/foo when referring to files within the same (logical) project as this
allows to move the project around without introducing changes in these imports.
Consider limiting the number of parent steps (../../../) as those can make
module and path structures hard to understand.
Namespace versus named imports
Both namespace and named imports can be used.
Prefer named imports for symbols used frequently in a file or for symbols that
have clear names, for example Jasmine's describe and it. Named imports can
be aliased to clearer names as needed with as.
Exports
Use named exports in all code: Do not use default exports. This ensures that all imports follow a uniform pattern.
Mutable exports
Regardless of technical support, mutable exports can create hard to understand
and debug code, in particular with re-exports across multiple modules. One way
to paraphrase this style point is that export let is not allowed.
If one needs to support externally accessible and mutable bindings, they
should instead use explicit getter functions.
For the common pattern of conditionally exporting either of two values, first do
the conditional check, then the export. Make sure that all exports are final
after the module's body has executed.
Import type
You may use import type {...} when you use the imported symbol only as a type.
Use regular imports for values:
import type {Foo} from './foo';
import {Bar} from './foo';
import {type Foo, Bar} from './foo';
Export type
Use export type when re-exporting a type, e.g.:
export type {AnInterface} from './foo';
Use modules not namespaces
TypeScript supports two methods to organize code: namespaces and modules,
but namespaces are disallowed. That
is, your code must refer to code in other files using imports and exports of
the form import {foo} from 'bar';
Your code must not use the namespace Foo { ... } construct. namespaces
may only be used when required to interface with external, third party code.
To semantically namespace your code, use separate files.
Code must not use require (as in import x = require('...');) for imports.
Use ES6 module syntax.
Use const and let
Always use const or let to declare variables. Use const by default, unless
a variable needs to be reassigned. Never use var.
Variables must not be used before their declaration.
Use readonly
Mark properties that are never reassigned outside of the constructor with the
readonly modifier (these need not be deeply immutable).
Properties used outside of class lexical scope
Properties used from outside the lexical scope of their containing class, such
as an Angular component's properties used from a template, must not use
private visibility, as they are used outside of the lexical scope of their
containing class.
TypeScript code must not use obj['foo'] to bypass the visibility of a
property.
Getters and setters
Getters and setters, also known as accessors, for class members may be used.
The getter method must be a
pure function (i.e., result is
consistent and has no side effects: getters must not change observable state).
They are also useful as a means of restricting the visibility of internal or
verbose implementation details (shown below).
If an accessor is used to hide a class property, the hidden property may be
prefixed or suffixed with any whole word, like internal or wrapped. When
using these private properties, access the value through the accessor whenever
possible. At least one accessor for a property must be non-trivial: do not
define "pass-through" accessors only for the purpose of hiding a property.
Instead, make the property public (or consider making it readonly rather than
just defining a getter with no setter).
Getters and setters must not be defined using Object.defineProperty, since
this interferes with property renaming.
Visibility
Restricting visibility of properties, methods, and entire types helps with keeping code decoupled.
- Limit symbol visibility as much as possible.
- Consider converting private methods to non-exported functions within the same file but outside of any class, and moving private properties into a separate, non-exported class.
- TypeScript symbols are public by default. Never use the
publicmodifier except when declaring non-readonly public parameter properties (in constructors).
Prefer function declarations for named functions
Prefer function declarations over arrow functions or function expressions when defining named functions. Arrow functions may be used, for example, when an explicit type annotation is required.
Arrow function bodies
Use arrow functions with concise bodies (i.e. expressions) or block bodies as
appropriate.
Only use a concise body if the return value of the function is actually used.
The block body makes sure the return type is void then and prevents potential
side effects.
Tip: The void operator can be used to ensure an arrow function with an
expression body returns undefined when the result is unused.
Prefer passing arrow functions as callbacks
Callbacks can be invoked with unexpected arguments that can pass a type check but still result in logical errors. Avoid passing a named callback to a higher-order function, unless you are sure of the stability of both functions' call signatures. Beware, in particular, of less-commonly-used optional parameters. Instead, prefer passing an arrow-function that explicitly forwards parameters to the named callback.
Prefer rest and spread when appropriate
Use a rest parameter instead of accessing arguments. Never name a local
variable or parameter arguments, which confusingly shadows the built-in name.
Use function spread syntax instead of Function.prototype.apply.
Control flow statements and blocks
Control flow statements (if, else, for, do, while, etc) always use
braced blocks for the containing code, even if the body contains only a single
statement. The first statement of a non-empty block must begin on its own line.
Exception: if statements fitting on one line may elide the block.
Iterating containers
Prefer for (... of someArr) to iterate over arrays. Array.prototype.forEach and vanilla for
loops are also allowed:
for (const x of someArr) {
// x is a value of someArr.
}
for (let i = 0; i < someArr.length; i++) {
// Explicitly count if the index is needed, otherwise use the for/of form.
const x = someArr[i];
// ...
}
for (const [i, x] of someArr.entries()) {
// Alternative version of the above.
}
Object.prototype.hasOwnProperty should be used in for-in loops to exclude
unwanted prototype properties. Prefer for-of with Object.keys,
Object.values, or Object.entries over for-in when possible.
Only throw errors
JavaScript (and thus TypeScript) allow throwing or rejecting a Promise with
arbitrary values. However if the thrown or rejected value is not an Error, it
does not populate stack trace information, making debugging hard. This treatment
extends to Promise rejection values as Promise.reject(obj) is equivalent to
throw obj; in async functions.
Instead, only throw (subclasses of) Error:
// Throw only Errors
throw new Error('oh noes!');
// ... or subtypes of Error.
class MyError extends Error {}
throw new MyError('my oh noes!');
// For promises
new Promise((resolve) => resolve()); // No reject is OK.
new Promise((resolve, reject) => void reject(new Error('oh noes!')));
Promise.reject(new Error('oh noes!'));
Catching and rethrowing
When catching errors, code should assume that all thrown errors are instances
of Error.
Exception handlers must not defensively handle non-Error types unless the
called API is conclusively known to throw non-Errors in violation of the above
rule. In that case, a comment should be included to specifically identify where
the non-Errors originate.
Empty catch blocks
It is very rarely correct to do nothing in response to a caught exception. When it truly is appropriate to take no action whatsoever in a catch block, the reason this is justified is explained in a comment.
Switch statements
All switch statements must contain a default statement group, even if it
contains no code. The default statement group must be last.
switch (x) {
case Y:
doSomethingElse();
break;
default:
// nothing to do.
}
Within a switch block, each statement group either terminates abruptly with a
break, a return statement, or by throwing an exception. Non-empty statement
groups (case ...) must not fall through (enforced by the compiler):
switch (x) {
case X:
doSomething();
// fall through - not allowed!
case Y:
// ...
}
Equality checks
Always use triple equals (===) and not equals (!==). The double equality
operators cause error prone type coercions that are hard to understand and
slower to implement for JavaScript Virtual Machines. See also the
JavaScript equality table.
Exception: Comparisons to the literal null value may use the == and
!= operators to cover both null and undefined values.
Type and non-nullability assertions
Type assertions (x as SomeType) and non-nullability assertions (y!) are
unsafe. Both only silence the TypeScript compiler, but do not insert any runtime
checks to match these assertions, so they can cause your program to crash at
runtime.
Because of this, you should not use type and non-nullability assertions
without an obvious or explicit reason for doing so.
When you want to assert a type or non-nullability the best answer is to
explicitly write a runtime check that performs that check.
Sometimes due to some local property of your code you can be sure that the
assertion form is safe. In those situations, you should add clarification to
explain why you are ok with the unsafe behavior:
If the reasoning behind a type or non-nullability assertion is obvious, the
comments may not be necessary. For example, generated proto code is always
nullable, but perhaps it is well-known in the context of the code that certain
fields are always provided by the backend. Use your judgement.
Type assertions and object literals
Use type annotations (: Foo) instead of type assertions (as Foo) to specify
the type of an object literal. This allows detecting refactoring bugs when the
fields of an interface change over time.
Dynamic code evaluation
Do not use eval or the Function(...string) constructor (except for code
loaders). These features are potentially dangerous and simply do not work in
environments using strict
Content Security Policies.
Modifying builtin objects
Never modify builtin types, either by adding methods to their constructors or to their prototypes. Avoid depending on libraries that do this. Do not add symbols to the global object unless absolutely necessary (e.g. required by a third-party API).
Descriptive names
Names must be descriptive and clear to a new reader. Do not use abbreviations that are ambiguous or unfamiliar to readers outside your project, and do not abbreviate by deleting letters within a word.
- Exception: Variables that are in scope for 10 lines or fewer, including arguments that are not part of an exported API, may use short (e.g. single letter) variable names.
Camel case
Treat abbreviations like acronyms in names as whole words, i.e. use
loadHttpUrl, not ~~loadHTTPURL~~, unless required by a platform name (e.g.
XMLHttpRequest).
_ prefix/suffix
Identifiers must not use _ as a prefix or suffix.
This also means that _ must not be used as an identifier by itself (e.g. to
indicate a parameter is unused).
Constants
Immutable: CONSTANT_CASE indicates that a value is intended to not be
changed, and may be used for values that can technically be modified (i.e.
values that are not deeply frozen) to indicate to users that they must not be
modified.
Global: Only symbols declared on the module level, static fields of module
level classes, and values of module level enums, may use CONST_CASE. If a
value can be instantiated more than once over the lifetime of the program (e.g.
a local variable declared within a function, or a static field on a class nested
in a function) then it must use lowerCamelCase.
Type inference
Code may rely on type inference as implemented by the TypeScript compiler for all type expressions (variables, fields, return types, etc).
const x = 15; // Type inferred.
Leave out type annotations for trivially inferred types: variables or parameters
initialized to a string, number, boolean, RegExp literal or new
expression.
Explicitly specifying types may be required to prevent generic type parameters
from being inferred as unknown. For example, initializing generic types with
no values (e.g. empty arrays, objects, Maps, or Sets).
const x = new Set<string>();
Nullable/undefined type aliases
Type aliases must not include |null or |undefined in a union type.
Nullable aliases typically indicate that null values are being passed around
through too many layers of an application, and this clouds the source of the
original issue that resulted in null. They also make it unclear when specific
values on a class or interface might be absent.
Instead, code must only add |null or |undefined when the alias is actually
used. Code should deal with null values close to where they arise, using the
above techniques.
Prefer optional over |undefined
In addition, TypeScript supports a special construct for optional parameters and
fields, using ?:
Optional parameters implicitly include |undefined in their type. However, they
are different in that they can be left out when constructing a value or calling
a method. For example, {sugarCubes: 1} is a valid CoffeeOrder because milk
is optional.
Use optional fields (on interfaces or classes) and parameters rather than a
|undefined type.
For classes preferably avoid this pattern altogether and initialize as many
fields as possible.
Use structural types
TypeScript's type system is structural, not nominal. That is, a value matches a type if it has at least all the properties the type requires and the properties' types match, recursively. When providing a structural-based implementation, explicitly include the type at the declaration of the symbol (this allows more precise type checking and error reporting). Use interfaces to define structural types, not classes
Prefer interfaces over type literal aliases
TypeScript supports type aliases for naming a type expression. This can be used to name primitives, unions, tuples, and any other types. However, when declaring types for objects, use interfaces instead of a type alias for the object literal expression.
Mapped and conditional types
TypeScript's
mapped types
and
conditional types
allow specifying new types based on other types. TypeScript's standard library
includes several type operators based on these (Record, Partial, Readonly
etc).
These type system features allow succinctly specifying types and constructing
powerful yet type safe abstractions. They come with a number of drawbacks
though:
The style recommendation is:
- Always use the simplest type construct that can possibly express your code.
- A little bit of repetition or verbosity is often much cheaper than the long term cost of complex type expressions.
- Mapped & conditional types may be used, subject to these considerations.
any Type
TypeScript's any type is a super and subtype of all other types, and allows
dereferencing all properties. As such, any is dangerous - it can mask severe
programming errors, and its use undermines the value of having static types in
the first place.
Consider not to use any. In circumstances where you want to use any,
consider one of:
- Provide a more specific type
- Use
unknown - Suppress the lint warning and document why
Using unknown over any
The any type allows assignment into any other type and dereferencing any
property off it. Often this behaviour is not necessary or desirable, and code
just needs to express that a type is unknown. Use the built-in type unknown in
that situation — it expresses the concept and is much safer as it does not allow
dereferencing arbitrary properties.
To safely use unknown values, narrow the type using a
type guard
{} Type
The {} type, also known as an empty interface type, represents a interface
with no properties. An empty interface type has no specified properties and
therefore any non-nullish value is assignable to it.
Google3 code should not use {} for most use cases. {} represents any
non-nullish primitive or object type, which is rarely appropriate. Prefer one of
the following more-descriptive types:
unknowncan hold any value, includingnullorundefined, and is generally more appropriate for opaque values.Record<string, T>is better for dictionary-like objects, and provides better type safety by being explicit about the typeTof contained values (which may itself beunknown).objectexcludes primitives as well, leaving only non-nullish functions and objects, but without any other assumptions about what properties may be available.
Wrapper types
There are a few types related to JavaScript primitives that should not ever be used:
String,Boolean, andNumberhave slightly different meaning from the corresponding primitive typesstring,boolean, andnumber. Always use the lowercase version.Objecthas similarities to both{}andobject, but is slightly looser. Use{}for a type that include everything exceptnullandundefined, or lowercaseobjectto further exclude the other primitive types (the three mentioned above, plussymbolandbigint). Further, never invoke the wrapper types as constructors (withnew).
@ts-ignore
Do not use @ts-ignore nor the variants @ts-expect-error or @ts-nocheck.
You may use @ts-expect-error in unit tests, though you generally should not.
@ts-expect-error suppresses all errors. It's easy to accidentally over-match
and suppress more serious errors. Consider one of:
- When testing APIs that need to deal with unchecked values at runtime, add
casts to the expected type or to
anyand add an explanatory comment. This limits error suppression to a single expression. - Suppress the lint warning and document why, similar to
suppressing
anylint warnings.
Conformance
These rules are commonly used to enforce critical restrictions (such as defining
globals, which could break the codebase) and security patterns (such as using
eval or assigning to innerHTML), or more loosely to improve code quality.
Google-style TypeScript must abide by any applicable global or framework-local
conformance rules.
JSDoc versus comments
There are two types of comments, JSDoc (/** ... */) and non-JSDoc ordinary
comments (// ... or /* ... */).
- Use
/** JSDoc */comments for documentation, i.e. comments a user of the code should read. - Use
// line commentsfor implementation comments, i.e. comments that only concern the implementation of the code itself.
Document all top-level exports of modules
Use /** JSDoc */ comments to communicate information to the users of your
code. Avoid merely restating the property or parameter name. You should also
document all properties and methods (exported/public or not) whose purpose is
not immediately obvious from their name, as judged by your reviewer.
Exception: Symbols that are only exported to be consumed by tooling, such as
@NgModule classes, do not require comments.
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.