Skip to content

Latest commit

 

History

History
344 lines (268 loc) · 11.3 KB

File metadata and controls

344 lines (268 loc) · 11.3 KB

Apps Script Sample Development Guide

This guide outlines best practices for developing Google Apps Script projects, focusing on type safety and modern JavaScript features.

Important

  • For new sample directories, ensure the top-level folder is included in the test.yaml GitHub 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 onOpen or main functions) by placing separate samples in their own directories or files. Do not append suffixes like _2, _3 to function names. For variables, replace collisions with a more descriptive name.

Tools

Lint and format code using Biome.

pnpm lint
pnpm format

Apps Script Code Best Practices

Apps Script supports the V8 runtime, which enables modern ECMAScript syntax. Using these features makes your code cleaner, more readable, and less error-prone.

let and const

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 here

Arrow Functions

Use arrow functions for concise function expressions, especially for callbacks.

const numbers = [1, 2, 3];
const squares = numbers.map(x => x * x); // [1, 4, 9]

Destructuring

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;

Template Literals

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.
`;

Default Parameters

Specify default values for function parameters.

function greet(name = "Guest") {
  console.log(`Hello, ${name}!`);
}

greet(); // "Hello, Guest!"

Prefer for...of for Iteration

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);
}

Apps Script V8 Runtime

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.

Unavailable APIs

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:

For some APIs, other workarounds might exist. For example, you might be able to use a polyfill for TextEncoder.

Asynchronous Limitations

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 and await resolutions occur) after the current call stack clears.
  • Macrotasks (Not Supported): Apps Script does not have a standard event loop for macrotasks. Functions like setTimeout() and setInterval() 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().

Class Limitations

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 or WeakMap for 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;).

Module Limitations

  • 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.

Type Checking with JSDoc

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.

Running Checks

You can run the type checker from the root of the repository.

Check all projects:

pnpm run check

Check 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/automations

Core Concepts

1. Basic Types

Use @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;
}

2. Apps Script Types

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();
}

3. Optional Parameters

Use [] or = to denote optional parameters.

/**
 * @param {string} name The name.
 * @param {number=} age Optional age.
 */
function greet(name, age) {
  if (age) { ... }
}

Advanced Patterns

1. Custom Objects (@typedef)

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);
}

2. Type Casting

Sometimes the type checker cannot infer the type correctly. Use inline @type to cast.

const data = JSON.parse(jsonString);

/** @type {UserConfig} */
const config = data;

3. Arrays and Generics

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);
}

4. Handling null and undefined

Be explicit if a value can be null.

/**
 * @param {string|null} id The ID, or null if not found.
 */
function find(id) { ... }

Common Issues & Fixes

  • 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 @typedef for that object structure.

  • Implicit 'any': If you see "Parameter 'x' implicitly has an 'any' type", it means you forgot a JSDoc @param tag. 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 using fields to limit the response.

    // Safe access
    console.log(user.name?.fullName);
  • Error Handling: Avoid wrapping code in try/catch blocks 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();