Work with JavaScript values and objects
Update an in-memory Apps Script request while keeping text, numbers, booleans, and missing values distinct without hidden type conversion.
Update a request record, compare values without hidden conversion, and choose defaults that preserve zero and false.
Represent a request without a spreadsheet
Update a room request in memory and inspect the results in the execution log. This standalone project uses the editor, function menu, and log from Run your first Sheets script, with its input defined directly in code.
The request has a title, a seat count supplied as text, an approval decision, an owner, a budget entry, and a cost. An object groups these named values into one record. Each named entry is a property: in title: 'Reserve a training room', title is the property name and the text after the colon is its value. Writing an object in source code with braces is called an object literal.
Quotes matter. '3' is a string containing text, even though its characters look like digits. 0 without quotes is a number used in arithmetic. false without quotes is a boolean (true or false); here false means the request has not been approved.
Strings, numbers, and booleans are primitive values. A primitive cannot be edited in place: replacing a title assigns a different string rather than mutating the old string. Objects, by contrast, have properties you can update. The other primitive values used here, null and undefined, represent different kinds of absence. MDN's data types guide explains this distinction between primitive values and objects.
Install and run the complete project
This project updates its in-memory record and writes messages through Logger.log() without reading or changing any Workspace data.
function inspectRequestValues() {
const request = {
title: 'Reserve a training room',
seatsText: '3',
approved: false,
owner: null,
budgetText: '',
cost: 0,
};
let stage = 'Draft';
const originalTitle = request.title;
const sameRequest = request;
const equalLookingRequest = {
title: 'Reserve a training room',
seatsText: '3',
approved: false,
owner: null,
budgetText: '',
cost: 0,
};
Logger.log('Equal titles: ' + (request.title === equalLookingRequest.title));
Logger.log('Same object: ' + (request === sameRequest));
Logger.log('Equal-looking objects: ' + (request === equalLookingRequest));
request.title = 'Reserve the large training room';
stage = 'Ready for review';
Logger.log('Original title: ' + originalTitle);
Logger.log('Title through sameRequest: ' + sameRequest.title);
Logger.log('Separate object title: ' + equalLookingRequest.title);
Logger.log('Stage: ' + stage);
const seats = Number(request.seatsText);
const seatsAsText = String(seats);
Logger.log('Seat text plus one: ' + (request.seatsText + 1));
Logger.log('Seat count plus one: ' + (seats + 1));
// Comparing text with a number is the point here, so widen the text's type.
/** @type {unknown} */
const seatInput = request.seatsText;
Logger.log('Seat text equals count: ' + (seatInput === seats));
Logger.log('Seat text differs from count: ' + (seatInput !== seats));
Logger.log('Converted count equals text: ' + (seatsAsText === request.seatsText));
// There is no reviewer property in the supplied record. The type note lets
// the editor accept reading a property that may be absent.
const reviewer = /** @type {{ reviewer?: string }} */ (request).reviewer;
Logger.log('Reviewer value: ' + String(reviewer));
Logger.log('Reviewer is undefined: ' + (reviewer === undefined));
Logger.log('Owner is null: ' + (request.owner === null));
Logger.log('Budget text is empty: ' + (request.budgetText === ''));
Logger.log('Cost is zero: ' + (request.cost === 0));
Logger.log('Approved is false: ' + (request.approved === false));
Logger.log('Reviewer with ??: ' + (reviewer ?? 'Not recorded'));
Logger.log('Owner with ??: ' + (request.owner ?? 'Unassigned'));
Logger.log('Budget with ??: <' + (request.budgetText ?? 'No budget supplied') + '>');
Logger.log('Budget with ||: <' + (request.budgetText || 'No budget supplied') + '>');
Logger.log('Cost with ??: ' + (request.cost ?? 25));
Logger.log('Cost with ||: ' + (request.cost || 25));
Logger.log('Approved with ??: ' + (request.approved ?? true));
Logger.log('Approved with ||: ' + (request.approved || true));
// Demonstrate the conversion trap without storing it as a budget.
Logger.log('Empty budget converted to number: ' + Number(request.budgetText));
}
The project manifest sets the V8 runtime and UTC time zone:
{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
Open
script.google.comand click New project. Name itRequest values. This is a standalone project, meaning it is not attached to a spreadsheet or another Workspace file, as described in Google's standalone-script guide.Replace the default code in
Code.gswith the completeCode.gsfile.In Project Settings, select Show "appsscript.json" manifest file in editor. Return to Editor, open
appsscript.json, and replace its contents with the included manifest. Save both files.Choose
inspectRequestValuesin the function menu and click Run.Open Execution log to read the messages.
The manifest is the project's configuration file. This manifest selects V8, the JavaScript runtime that supports const and let declarations; see Google's V8 runtime guide.
inspectRequestValues runs as the signed-in user who clicks Run. Because the code uses only JavaScript values and Logger, it requires no OAuth scopes or consent prompt. Apps Script scans all files in a project to detect service scopes; Google's authorization guide explains both scope detection and execution identity.
Recognize the supplied result
Expect these 27 message bodies in order (ignoring editor timestamps and execution notices). The angle brackets in the budget messages make an empty string visible as <>.
Equal titles: true
Same object: true
Equal-looking objects: false
Original title: Reserve a training room
Title through sameRequest: Reserve the large training room
Separate object title: Reserve a training room
Stage: Ready for review
Seat text plus one: 31
Seat count plus one: 4
Seat text equals count: false
Seat text differs from count: true
Converted count equals text: true
Reviewer value: undefined
Reviewer is undefined: true
Owner is null: true
Budget text is empty: true
Cost is zero: true
Approved is false: true
Reviewer with ??: Not recorded
Owner with ??: Unassigned
Budget with ??: <>
Budget with ||: <No budget supplied>
Cost with ??: 0
Cost with ||: 25
Approved with ??: false
Approved with ||: true
Empty budget converted to number: 0 Several lines illustrate bugs to avoid in real scripts: '3' + 1 produces 31 instead of 4, and the || fallback overrides the recorded 0 cost and false approval.
Keep a name, change a property
A variable gives a value a name. const request = ... creates a binding that cannot be reassigned to a different value. let stage = 'Draft' creates a binding that can be reassigned, so the script later updates stage to 'Ready for review'. Use const by default and let when you need to reassign the variable.
const does not freeze an object. request.title = 'Reserve the large training room' updates a property on the existing object, whereas reassigning request to a different object would throw an error. MDN's const reference distinguishes a fixed binding from an object's mutable properties.
Dot notation reads or updates a named property (request.title), and bracket notation (request['title']) accesses the same property by string name. Property names are case-sensitive, so request.Title refers to a different property.
Saving request.title in originalTitle copies the string value before the update, so originalTitle still logs Reserve a training room after request.title changes.
Distinguish one object from matching data
const sameRequest = request creates a second reference to the same object rather than copying the record. Reading sameRequest.title after updating request.title returns the new title because both variables point to the same object.
equalLookingRequest starts with a second object literal. Its properties initially match request, but it is a separate object in memory, so changing request.title leaves equalLookingRequest.title unchanged.
The strict equality operator, ===, compares two values without converting their types. For strings, it checks whether the characters match. For objects, it checks identity (whether both expressions reference the same object in memory), not whether their properties match. That is why the first three messages log true, true, and false. MDN's strict equality reference documents both rules.
Use request === sameRequest to check whether two variables refer to the same object, or compare individual properties such as request.title === equalLookingRequest.title.
Convert text before arithmetic
The input seatsText holds '3'. Adding 1 to a string concatenates text: JavaScript converts 1 to '1' and produces '31'. Converting the string first with Number(request.seatsText) produces the number 3, so seats + 1 evaluates to 4 while leaving request.seatsText unchanged.
Automatic type conversion is called coercion. It also occurs when log expressions concatenate a string label with a boolean or number. The parentheses in (seats + 1) force the addition to run before string concatenation. MDN's discussion of implicit conversion explains how mixing strings and numbers changes operator behavior.
Strict comparison does not coerce types: '3' === 3 is false, and strict inequality ('3' !== 3) is true. Likewise, 0 === false is false. Prefer === and !== over == and !=, which coerce values before comparing them.
In the other direction, String(seats) returns '3', so seatsAsText === request.seatsText is true. Call String(value) and Number(value) without new to convert primitive values.
Number() converts both '' and whitespace-only strings to 0, as shown by the final log line (Empty budget converted to number: 0). Non-numeric text such as 'three' converts to NaN ("not a number"). Check for blank strings before calling Number() so a missing input is not mistaken for 0.
Give each empty value a meaning
Because request has no reviewer property, reading request.reviewer returns undefined (as does reading a misspelled property name). MDN's undefined type explanation describes missing-property lookups.
The owner property exists and is set to null to represent an unassigned owner. Under strict comparison, null and undefined are distinct: use request.owner === null to check specifically for null.
The remaining three properties hold distinct non-nullish values: budgetText is the empty string '', cost is the number 0, and approved is the boolean false. Each matches only its own value and type under ===.
Choose a fallback without losing zero or false
The nullish coalescing operator, ??, returns its right-hand operand only when its left-hand operand is null or undefined (nullish). With ??, the missing reviewer logs Not recorded and the null owner logs Unassigned.
A truthiness check evaluates any value as boolean true or false. Empty strings (''), 0, false, null, and undefined are all falsy, while non-empty strings (including 'false' and '0') are truthy.
When used for defaults, logical OR (||) returns its right-hand operand whenever the left-hand operand is falsy. That replaces cost: 0 with 25 and approved: false with true, whereas ?? preserves both 0 and false. MDN's nullish coalescing reference compares ?? with ||.
For budgetText, ?? preserves '' (logged as <>) because an empty string is not nullish, while || substitutes No budget supplied because '' is falsy. Use ?? when 0 or false is a valid value, and check for empty strings separately when blank text needs a fallback.
Change the cost and predict the result
In the first
requestobject, changecost: 0tocost: 10. LeaveequalLookingRequestand the log expressions unchanged.Predict the three
Costmessages before running:10is non-zero, non-nullish, and truthy, so expectCost is zero: false,Cost with ??: 10, andCost with ||: 10.Save and run
inspectRequestValuesto verify those three lines.Restore
cost: 0, save, and rerun to return to the baseline output.
If inspectRequestValues is missing from the function menu, check that Code.gs contains the complete example and is saved. If the editor reports a syntax error after an edit, check commas, braces, and straight quotes.
Each execution initializes fresh objects from the saved source; mutating request.title in memory does not modify Code.gs or persist to the next run. Next, use functions and control flow to make decisions from a request record.
Project files
Complete source files for this chapter’s examples.
Inspect and change an in-memory request
Code.gs
Download filefunction inspectRequestValues() {
const request = {
title: 'Reserve a training room',
seatsText: '3',
approved: false,
owner: null,
budgetText: '',
cost: 0,
};
let stage = 'Draft';
const originalTitle = request.title;
const sameRequest = request;
const equalLookingRequest = {
title: 'Reserve a training room',
seatsText: '3',
approved: false,
owner: null,
budgetText: '',
cost: 0,
};
Logger.log('Equal titles: ' + (request.title === equalLookingRequest.title));
Logger.log('Same object: ' + (request === sameRequest));
Logger.log('Equal-looking objects: ' + (request === equalLookingRequest));
request.title = 'Reserve the large training room';
stage = 'Ready for review';
Logger.log('Original title: ' + originalTitle);
Logger.log('Title through sameRequest: ' + sameRequest.title);
Logger.log('Separate object title: ' + equalLookingRequest.title);
Logger.log('Stage: ' + stage);
const seats = Number(request.seatsText);
const seatsAsText = String(seats);
Logger.log('Seat text plus one: ' + (request.seatsText + 1));
Logger.log('Seat count plus one: ' + (seats + 1));
// Comparing text with a number is the point here, so widen the text's type.
/** @type {unknown} */
const seatInput = request.seatsText;
Logger.log('Seat text equals count: ' + (seatInput === seats));
Logger.log('Seat text differs from count: ' + (seatInput !== seats));
Logger.log('Converted count equals text: ' + (seatsAsText === request.seatsText));
// There is no reviewer property in the supplied record. The type note lets
// the editor accept reading a property that may be absent.
const reviewer = /** @type {{ reviewer?: string }} */ (request).reviewer;
Logger.log('Reviewer value: ' + String(reviewer));
Logger.log('Reviewer is undefined: ' + (reviewer === undefined));
Logger.log('Owner is null: ' + (request.owner === null));
Logger.log('Budget text is empty: ' + (request.budgetText === ''));
Logger.log('Cost is zero: ' + (request.cost === 0));
Logger.log('Approved is false: ' + (request.approved === false));
Logger.log('Reviewer with ??: ' + (reviewer ?? 'Not recorded'));
Logger.log('Owner with ??: ' + (request.owner ?? 'Unassigned'));
Logger.log('Budget with ??: <' + (request.budgetText ?? 'No budget supplied') + '>');
Logger.log('Budget with ||: <' + (request.budgetText || 'No budget supplied') + '>');
Logger.log('Cost with ??: ' + (request.cost ?? 25));
Logger.log('Cost with ||: ' + (request.cost || 25));
Logger.log('Approved with ??: ' + (request.approved ?? true));
Logger.log('Approved with ||: ' + (request.approved || true));
// Demonstrate the conversion trap without storing it as a budget.
Logger.log('Empty budget converted to number: ' + Number(request.budgetText));
}
appsscript.json
Download file{
"timeZone": "Etc/UTC",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}