Work with JavaScript values and objects
Update a request object in Apps Script and log how strings, numbers, booleans, null, and undefined compare, convert, and fall back with ?? and ||.
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 held in memory and read the results in the execution log. You need the Apps Script editor, function menu, and log from Run your first Sheets script. This project is standalone and its input is written directly in Code.gs, so no spreadsheet is involved.
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. To change the title, you assign a different string. The original string stays unchanged.
An object's properties can be updated in place. MDN's data types guide explains this distinction between primitive values and objects. The other primitive values used here, null and undefined, represent different kinds of absence.
Install and run the complete project
This project updates its in-memory record and writes messages to the execution log through Logger.log(). It does not read or change 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. A standalone project is not attached to a spreadsheet or another Workspace file (see 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 how scopes are detected and which account a run uses.
Recognize the supplied result
The log shows these 27 messages in this order, each after a timestamp. 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 = ... declares a variable that cannot be reassigned to a different value. let stage = 'Draft' declares one that can be reassigned, so the script later sets 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. Reassigning request itself to a different object would throw an error. MDN's const reference explains that const fixes the variable, not the object's 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 gives the same object a second name. It does not copy the record. Reading sameRequest.title after updating request.title returns the new title because both variables point to the same object.
equalLookingRequest is created from 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 sides are the same object in memory. Separate objects with matching properties are not equal.
The first three messages therefore 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 joins 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. The conversion leaves request.seatsText unchanged.
Automatic type conversion is called coercion. It also happens when a log expression joins a string label with a boolean or number. The parentheses in (seats + 1) make JavaScript add the numbers before it joins the result to the label. 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 the script does not treat a missing input as 0.
Give each empty value a meaning
Because request has no reviewer property, reading request.reviewer returns undefined. Reading a misspelled property name returns undefined for the same reason. 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 values that are neither null nor undefined: 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. In this project, || 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 original 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 creates fresh objects from the saved source. Changing request.title in memory does not modify Code.gs and does not carry over to the next run. Next, read Make decisions with functions and loops to decide what to do with 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"
}