This guide outlines best practices for developing Google Apps Script projects, focusing on type safety and modern JavaScript features.
- For new sample directories, ensure the top-level folder is included in the
test.yamlGitHub workflow's matrix configuration. - Do not move or delete snippet tags:
[END apps_script_... ]or[END apps_script_... ]. - Keep code within snippet tags self-contained. Avoid depending on helper functions defined outside the snippet tags if the snippet is intended to be copied and pasted.
- Avoid function name collisions (e.g., multiple
onOpenormainfunctions) by placing separate samples in their own directories or files. Do not append suffixes like_2,_3to function names. For variables, replace collisions with a more descriptive name.
Lint and format code using Biome.
pnpm lint
pnpm formatApps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone.
Use let and const instead of var for block-scoped variables.
const: Use for values that should not be reassigned.let: Use for values that will change.
const PI = 3.14;
let count = 0;
if (true) {
let local = "I exist only in this block";
}
// local is not accessible hereUse arrow functions for concise function expressions, especially for callbacks.
const numbers = [1, 2, 3];
const squares = numbers.map(x => x * x); // [1, 4, 9]Unpack values from arrays or properties from objects into distinct variables.
const user = { name: "Alice", age: 30 };
const { name, age } = user;
const coords = [10, 20];
const [x, y] = coords;Use template literals for string interpolation and multi-line strings.
const name = "World";
const greeting = `Hello, ${name}!`;
const multiLine = `
This is a
multi-line string.
`;Specify default values for function parameters.
function greet(name = "Guest") {
console.log(`Hello, ${name}!`);
}
greet(); // "Hello, Guest!"While forEach is convenient, for...of loops generally offer better performance and more control (e.g., break, continue) in Apps Script, especially when dealing with large arrays.
const numbers = [1, 2, 3];
// Using forEach (less performant for large arrays)
numbers.forEach(num => {
console.log(num);
});
// Using for...of (preferred)
for (const num of numbers) {
console.log(num);
}It's important to understand that the Apps Script V8 runtime is not a standard Node.js or browser environment. This can lead to compatibility issues when incorporating third-party libraries or adapting code examples from other JavaScript environments.
The following standard JavaScript APIs are NOT available in the Apps Script V8 runtime:
- Timers:
setTimeout,setInterval,clearTimeout,clearInterval - Streams:
ReadableStream,WritableStream,TextEncoder,TextDecoder - Web APIs:
fetch,FormData,File,Blob,URL,URLSearchParams,DOMException,atob,btoa - Crypto:
crypto,SubtleCrypto - Global Objects:
window,navigator,performance,process(Node.js)
Instead of the unavailable APIs, you can use the following Apps Script APIs as alternatives:
- Timers: Use
Utilities.sleep(milliseconds)for synchronous pauses. Asynchronous timers are not supported. - Fetch: Use
UrlFetchApp.fetch(url, params)to make HTTP(S) requests. - atob: Use
Utilities.base64Decode()to decode Base64-encoded strings. - btoa: Use
Utilities.base64Encode()to encode strings in Base64. - Crypto: Use
Utilitiesfor cryptographic functions likecomputeDigest(),computeHmacSha256Signature(), andcomputeRsaSha256Signature().
For some APIs, other workarounds might exist. For example, you might be able to
use a polyfill for TextEncoder.
The V8 runtime supports async and await syntax and the Promise object.
However, the Apps Script runtime environment is fundamentally
synchronous.
- Microtasks (Supported): The runtime processes the microtask queue (where
Promise.then()callbacks andawaitresolutions occur) after the current call stack clears. - Macrotasks (Not Supported): Apps Script does not have a
standard event loop for macrotasks. Functions like
setTimeout()andsetInterval()are not available. - WebAssembly Exception: The WebAssembly API is the only built-in feature that operates in a non-blocking manner within the runtime, allowing for specific asynchronous compilation patterns (WebAssembly.instantiate).
All I/O operations, such as
UrlFetchApp.fetch(), are
blocking. To achieve parallel network requests, use
UrlFetchApp.fetchAll().
The V8 runtime has specific limitations regarding modern ES6+ class features:
- Private Fields: Private class fields (for example,
#field) are not supported and cause parsing errors. Consider using closures orWeakMapfor true encapsulation. - Static Fields: Direct static field declarations within the class body
(for example,
static count = 0;) are not supported. Assign static properties to the class after its definition (for example,MyClass.count = 0;).
- ES6 Modules: The V8 runtime does not support ES6 modules (
import/export). To use libraries, you must either use the Apps Script library mechanism or bundle your code and its dependencies into a single script file. (Issue Tracker) - File Execution Order: All script files in your project are executed in a global scope. It's best to avoid top-level code with side effects and ensure functions and classes are defined before being used across files. Explicitly order your files in the editor if dependencies exist between them.
This project uses a type checker to validate .gs files for errors. Since .gs files are technically JavaScript, we use JSDoc comments to provide type information. This ensures your code is type-safe and well-documented.
You can run the type checker from the root of the repository.
Check all projects:
pnpm run checkCheck a specific path:
To check only projects within a specific directory (e.g., solutions/automations), pass the path as an argument:
pnpm run check solutions/automationsUse @param and @return to define function inputs and outputs.
/**
* Adds two numbers.
* @param {number} a The first number.
* @param {number} b The second number.
* @return {number} The sum.
*/
function add(a, b) {
return a + b;
}You can reference global Apps Script types directly.
/**
* Gets the active sheet name.
* @return {string} The name of the sheet.
*/
function getSheetName() {
// Types like SpreadsheetApp, Sheet, Range are available globally
const sheet = SpreadsheetApp.getActiveSheet();
return sheet.getName();
}Use [] or = to denote optional parameters.
/**
* @param {string} name The name.
* @param {number=} age Optional age.
*/
function greet(name, age) {
if (age) { ... }
}For complex objects, define a type using @typedef.
/**
* @typedef {Object} UserConfig
* @property {string} username The user's name.
* @property {boolean} isAdmin Whether the user is an admin.
* @property {number} [retryCount] Optional retry attempts.
*/
/**
* Processes a user configuration.
* @param {UserConfig} config The configuration object.
*/
function processUser(config) {
console.log(config.username);
}Sometimes the type checker cannot infer the type correctly. Use inline @type to cast.
const data = JSON.parse(jsonString);
/** @type {UserConfig} */
const config = data;Specify array contents clearly.
/**
* @param {string[]} names An array of strings.
* @return {Array<number>} An array of numbers.
*/
function lengths(names) {
return names.map(n => n.length);
}Be explicit if a value can be null.
/**
* @param {string|null} id The ID, or null if not found.
*/
function find(id) { ... }-
TypeScript: DO NOT REFERENCE GoogleAppsScript in JSDocs. Instead use a locally defined type definition and link to the appropriate reference documenation page if possible.
-
"Property 'x' does not exist on type 'Object'": This usually means you are accessing a property on a generic object. Define a
@typedeffor that object structure. -
Implicit 'any': If you see "Parameter 'x' implicitly has an 'any' type", it means you forgot a JSDoc
@paramtag. Add it to fix the error. -
Advanced Services: To fix errors with these globals, check for existence. This helps TypeScript narrow the type and prevents runtime errors if the service is not enabled.
if (!AdminDirectory) { console.log('AdminDirectory Advanced Service must be enabled.'); return; }
-
Optional Properties: Use optional chaining (
?.) when accessing properties that might be undefined in API responses. This is often the case when when usingfieldsto limit the response.// Safe access console.log(user.name?.fullName);
-
Error Handling: Avoid wrapping code in
try/catchblocks if you are only logging the error message. Let the runtime handle the error reporting for cleaner sample code.// Avoid this try { AdminDirectory.Users.list(); } catch (err) { console.log(err.message); } // Prefer this AdminDirectory.Users.list();