software development blog
February 19, 2022

node coding handbook

this document is a cheat sheet for developing in node using mainly typescript. It covers things like language features, best practices, and some traps and gotchyas. It concentrates on the major improvements to the core javascript language that have been added over the past 10 years or so. I keep updating this document with new tips and tricks as I find them.

jsdoc

a standard way of documenting your code so that it can be parsed and used to build VSCode's Intellisense hints or a standalone documentation website

  • comments starting with /** will become part of the documentation for the code following it
  • special @ tags can be inserted into the comment to denote special items, like @constructor, and @param
  • links to other parts of the code docs are done like this: {@link MyClass#member}

variable declaration

var • scoped to the function block it's located in, even if declared inside of a loop; deprecated in favor of let

let • scoped to current block only (tip: you can even make a variable local to only the inside of a while loop, or an if statement)

const • scoped to current block and below only

a single var, let, or const can contain multiple declarations separated by commas. ex:

let x = 1, y =2, z = 3;

basic types

note: when assigning a value at declaration (with const, let, or var) no need to include the type since it's inferred by the value. (ex. let s = 'definitely a string')

  • number • you can use the constant Number.MAX_SAFE_INTEGER to represent the highest possible integer (which is a little over 9 quadrillion)
  • boolean
  • string
  • any • special type that tells typescript that the variable may contain any type, including undefined and null
  • enum • enumeration type enum enumName {item1, item2, etc...}
  • type aliases, and the or operator
    type StringOrNull = string | null;
    this creates a type that can be either a string or a null

variable truthiness and falsiness

in boolean operations, there are other values besides false itself that are evaluated as false. These are known as "falsy" values: undefined, null, NaN, 0, and '' (empty string). All other values are "truthy" values. Examples of truthy values: numbers other than 0 and NaN, object references, and non empty strings.

variable existence and nullishness

all variables (both literal and object references) in JavaScript can have 4 different states!

1. non-existing / undeclared

  • if a variable was never declared in the current scope, it is non-existing. If you try to reference that variable, an exception is thrown. For example. if (someRandomVariable == 1) console.log('this will never get here, it will throw an exception.');

2. undefined

  • if a variable was declared, but not assigned a value, it is undefined. for example: let x; if (x == 1) console.log('this will never get here, but it will not throw an exception.');
  • it is preferred to use undefined instead of null to indicate that a variable has no value assigned

3. null

  • a value you can assign to any variable to indicate an empty state. for example: let x = null; if (x == 1) console.log('this will never get here, but it will not throw an exception.');
  • Unfortunately this is very similar to undefined and in my opinion it is unnecessary to have both null and undefined as part of the language. This is why JS developers have adopted the term "nullish", which conceptually combines both undefined and null.
  • is most cases, using undefined is preferred to using null

4. and, of course, a concrete value

  • for example: let x = 1; if (x == 1) console.log('yes, x == 1.');

checking for undefined or null (aka "nullishness")

if (variable == null) console.log('the variable is currently set to *either* null or undefined.')
similarly:
if (variable == undefined) console.log('the variable is currently set to *either* null or undefined.')

  • since the variable was declared and available to the scope of the if statement, this will tell you if the variable is currently undefined or null. If the variable was never declared, this would throw an exception.
  • be careful about using === because when x is undefined, (x === null) is false! Only use == and not === if testing for "nullishness"

checking for existence

  • if (typeof variable == 'undefined') console.log('variable either doesn't exist at all, or is currently set to undefined')

  • using the typeof keyword as shown checks if the variable is non-existent, or does exist but is set to undefined (and not null, since if it was null, typeof would return 'object'!!). Usually this is good enough, but if you really just need to know if the variable exists or not regardless of what it is set to, (undefined, null, or a concrete value) you must use a try-catch block:

  • try { if (variable) {} } catch { console.log('the variable does not exist at all!'); }

checking for existence, undefined and null all at once

  • if ( typeof x == 'undefined' || x == null ) console.log('x is either non-existent, undefined, or null.');

  • the typeof x == 'undefined' part checks for existence (and undefined) and the x == null checks for null (and undefined). It's easy to be confused by the necessity of this, because the above ends up checking for undefined twice, but that can't be helped.

shortcut for checking if an object is assigned to a variable

  • if (x) console.log('x is assigned.')

  • don't use this to test if strings, numbers, or booleans (i.e. non-objects) are not nullish because empty string, and the number 0 both evaluate to false, and of course the boolean value of false evaluates to false.

shortcut for checking if a string is empty, null or undefined

  • if (s) console.log('s is assigned a non empty string.')

  • don't worry, a string of '0' or 'false' will not evaluate to a boolean false (it might in other languages however, like PHP)

shortcut for checking if an array has values (is not empty)

  • if (arr.length) console.log('array arr has at least 1 value')

operators

+ unary operator - converts the expression that follows it to a number: let num = +'-10' (num now equals -10 of type number)

- unary operator - converts the expression that follows it to a number, then multiplies by -1: let num = -'-10' (num now equals 10)

~ unary operator - inverts the bits in a number, often used as a shortcut hack in Javascript because ~-1 == 0. You might see if (~s.indexOf('test')) which takes the place of if (s.indexOf('test') != -1)

|| • falsy coalescing logical operator: let x = ''; x = x || 'A'; x will equal 'A' because x was falsy

?? • nullish coalescing logical operator: let x; x = x ?? 4; x will equal 4 because x was undefined or null

??= • nullish coalescing assignment: let x; x ??= 4 is the same as let x; x = x ?? 4;

?. • optional chaining operator: let x; let y = x?.notThere; no undefined error occurs, y will simply be undefined

?.() • optional chaining function call! let f:(s:string)=>void; f?.('hi'); only executes if f is assigned!

?.[] • optional chaining array access! let y = arr?.[1] ?? ''; y == '' if the array is nullish or element 1 is nullish!

[] • computed property name: let a = 'propName'; let b = {[a]:1}; same as let b = {propName: 1}

{} • object destructuring: let {a,b,c} = obj; same as let a = obj.a; let b = obj.b; let c = obj.c;
can rename the pulled out properties like this: let {a:var1,b:var2,c:var3} = obj;

[] • array destructuring: let [a,b,c] = arr; same as let a= arr[0]; let b= arr[1]; let c= arr[2];

… • "spread" operator

  • use to expand an array into individual elements: a = [1,2,3]; b = [7,8,9]; all = [...a,4,5,6,...b];
  • works the same for objects and the members inside of it and can be useful for object cloning: x = {a:1;b:2}; y = x; in this case y is a reference to x.
    instead, y = {...x}; now y is a new copy of x;
  • also see "rest operator" in function parameters which is similar

bitwise operators - they're strange

because javascript's number format is a mishmosh of integers of varying bit lengths and floating point numbers, the bitwise operators are a little strange. First note that although javascript's integer mode for its number type can store its values in bit fields as large as 54 bits, the bitwise operators only work on 32 bit integers. If you try to use a bitwise operator on an integer greater than 32 bits, the extra significant bits simply get truncated.

truncation example:
  333322222222221111111111                     33322222222221111111111         
  321098765432109876543210987654321            21098765432109876543210987654321
0b110010000000000000000000000000000 >>> 0 == 0b10010000000000000000000000000000 // (bit #33 is lost)

additionally, most of the bitwise operations return signed 32 bit integers. (where if a 1 in bit #32 is present, it indicates that the number will be interpreted as a two's compliment negative integer.) This can be very confusing. For example, if you are using the << shift-right operator, and you move a 1 into the 32nd bit, your result will become a negative integer. Comparisons with that result may yield incorrect results if you are expecting a positive 32 bit integer to be the result.

the operators:

&• bitwise "and", if bit #32 becomes a 1, the result is a negative integer
|• bitwise "or", if bit #32 becomes a 1, the result is a negative integer
^• bitwise "xor", if bit #32 becomes a 1, the result is a negative integer
~• bitwise "not", if bit #32 becomes a 1, the result is a negative integer
>>>• "normal" bitwise shift right - shifts a number's bits to the right by one, making bit #32 a 0. Unlike all other operators, this always returns an unsigned 32 bit integer -- even if there is a 1 in bit #32 after the operation, like when using xyz >>> 0. (one trick is to usexyz >>> 0 to convert a result from any of the other bitwise operations into an unsigned 32 bit integer.)
>> • "strange" bitwise shift right - shifts a number's bits to the right by one, but additionally, if bit #32 is a 1, then the bit #32 stays a 1
<< • "semi-strange" bitwise shift left - shifts a number's bits to the left by one, and sets bit #1 to 0. But if bit #32 becomes a 1, the result is a negative integer.

"normal" bitwise shift right example:
  33322222222221111111111                     33322222222221111111111         
  21098765432109876543210987654321            21098765432109876543210987654321
0b10010000000000000000000000000000 >>> 1 == 0b01001000000000000000000000000000 // obviously, a positive integer

"strange" bitwise shift right example:
  33322222222221111111111                    33322222222221111111111         
  21098765432109876543210987654321           21098765432109876543210987654321
0b10010000000000000000000000000000 >> 1 == 0b11001000000000000000000000000000 // becomes a negative integer

 "semi-strange" bitwise shift left:
  33322222222221111111111                    33322222222221111111111         
  21098765432109876543210987654321           21098765432109876543210987654321
0b01001000000000000000000000000000 << 1 == 0b10010000000000000000000000000000 // becomes a negative integer

a trick to always get an unsigned integer example:
  33322222222221111111111                     33322222222221111111111         
  21098765432109876543210987654321            21098765432109876543210987654321
0b10010000000000000000000000000000 >>> 0 == 0b10010000000000000000000000000000 // always a positive integer

type assertions

(<theType>theVariable).whatever()
(theVariable as theType).whatever()

this is not typecasting, it is only to help fix compile-time errors when typescript can not be sure the type is what you know it to be.

complex types

interfaces

  • object / class interface:
    interface MyInterface {a:number; b:string}
  • function interface:
    interface MyInterface { (a,b:number; c:string):boolean }
  • generics:
    interface MyStack<T> { function push(item:T) {} }

classes

  • declaration:
    class MyClass implements MyInterface {}
    class MyClass extends ParentClass {}
    private - can be added to front of member declarations (default is public)
    protected - members can only be accessed by sub classes
    public - optional access modifier, not needed since it's the default
    static - can be added in addition to the above to create a static class member

  • to create a class instance: let instanceVariable = new MyClass();

  • methods: can be defined without function keyword
    class MyClass { private myMethod() { return 'hi!';} }

  • constructor(): must be used as the name of the constructor function, you can prefix constructor params with public or private to make them become members of the actual class!
    example: constructor(public member1:string)
    call parent constructor like: super(<props>);
    call inherited props and methods like: super.member1 = '';

  • properties:
    can have properties with real getters and setters!
    get propName(){return this._propName;}
    set propName(value:any){this._propName = value;}
    then you can do this: myObj.propName = myObj.propName + 1

  • this
    depends on how the method is called
    if using object.method(), this behaves normally, i.e., this = reference to the object
    for indirect calls like: x = object.method; x(); the this inside of x() will be undefined!
    to fix, must assign variables like so: x = object.method.bind(object);

  • function types:
    function getNum():number { //... } -- only necessary if the return doesn't have an obvious type

  • function parameters:
    function f(param1?:string) - use ? to indicate optional parameter
    function f(active = true) - default parameters! --and in this example we don't need to explicitly specify boolean
    function f(param1:string, ...otherParams: string[])
    above is called "rest params", this puts the "rest" of the params into the otherParams array.

  • function overloads:
    can be done in 2 ways:
    1) simple: use intersection types like f(a:number|string)
    2) more complex: stack up multiple declarations, with the last one using compound types, or 'any'

    then only implement the last one:

    function addItems(a:string, b:object): object;
    function addItems(a:string, c:string): string;
    function addItems(a:string, bc: object|string): object|string {
      ...implementation code...
    }
    

    • notice that you cannot have separate implementations for each overload
    • the last declaration's parameters must be able to cover all of the overload scenarios
    • the names of the parameters don't have to match, only the order of the parameters and the types matter -- notice how bc covers both the b parameter and the c parameter.

  • arrow functions:
    (param1,param2) => {param1.whatever(); param2.whatever(); return x;}
    above shows the need for parens and braces for multiple params and statements
    param => param.whatever()
    parens and braces not needed when only one param and/or one statement, the value is automatically returned
    this - refers to the calling block/function's this

  • template strings:
    use backticks `to make a template string which can span multiple lines.`
    can use template literals to insert variables `My var is: ${myVar}`

  • template tags:
    you can call a function on templates like so: tagFn`My var is: ${myVar}`
    you would then implement this:
    function tagFn(strings:TemplateStringsArray, ...params[])
    strings is a list of all strings broken by any ${…} parameters passed
    params are the values of each ${…} parameter.
    in the example, strings[0] would be 'My var is: ', and params[0] would be the value of myVar

exceptions

try {} catch(e) {} finally {}
e is either an instance of the standard Error object, or the object instantiated by a particular throw
throw new Error('whatever'); - raises an exception

modules

modules are units of code aka code libraries. Variables and classes are all scoped to the module they are defined in, even if you use var. Use export keyword to make variables and classes available to be imported outside of the module. Use export default to indicate the default export. import - allows you to use variables and classes from other modules. ex. import defaultExport, { namedExport1, ...} from './path/moduleNameNoExt'.

module resolution

a very important and tricky aspect to modules is "module resolution". i.e. how typescript resolves the true disk location of a specified module. There are 3 ways to specify the location of the module when using an import statement. We'll call the part of the import statement that identifies the module, the "module specifier". The following types of "module specifier" are possible:

  1. "absolute path" - this is when the path begins with a slash, or drive letter. i.e. the full path of the module file for your operating system. This is pretty much never used, because moving your project to another machine or just to another folder on the same machine would be impractical. This is especially bad if sharing your code with other developers.

  2. "relative path" - this is when the path begins with a single or double dot. Typescript will look for the module based on the relative path starting from the importing module's position in the directory tree. Generally, all of your app specific modules will be defined this way, especially for simple projects. This is very straight forward, however, if rootDirs if specified in tsconfig.json, typescript will consider the combination of all the path entries in rootDirs as one giant virtual folder. Every module in that virtual folder can access every other module with './theModuleName' as if the modules all resided in the same folder.

  3. "findable path" - this is by far the most complex way, and the most useful way. This type of module resolution is activated by using a partial path to the module (one not beginning with dots, slashes or drive letters). It prompts typescript to search in a number of locations. Note that folders may not actually be included in the module specifier because simply using a bare module name is possible and very common. This is mainly used to import third party modules that were installed into node_modules via npm. Note: the typescript documentation refers to this kind of path as a "non-relative" path, however, I don't like that phrasing because it's too confusing. It's better to call something by what it is rather than what it is not. Especially when "non-relative" would ordinarily mean "absolute" under most operating systems.

    The actual search is done using the following algorithm:

   /*
     in this example, the importing module is /myProject/src/moduleA.ts
     and tsconfig.json is here: /myProject/tsconfig.json
     (although, tsconfig's location is immaterial in this example)
   */
   import * as modB from 'moduleB';

   // The following locations are tried by typescript when looking for moduleB
   1. /myProject/src/node_modules/moduleB.ts
   2. /myProject/src/node_modules/moduleB.tsx
   3. /myProject/src/node_modules/moduleB.d.ts
   4. /myProject/src/node_modules/moduleB/package.json   // (to see if it specifies a "types" property)
   5. /myProject/src/node_modules/@types/moduleB.d.ts
   6. /myProject/src/node_modules/moduleB/index.ts
   7. /myProject/src/node_modules/moduleB/index.tsx
   8. /myProject/src/node_modules/moduleB/index.d.ts

   /*
   Then it will try this entire search again for /myProject/node_modules/..., and /node_modules/...
   (That is, it will search for node_modules folders up the entire tree even into the root folder of the hard drive--which is a little crazy.)
   */

   // ...and if the import includes a path...
   import * as modB from 'mods/moduleB';
   1. /myProject/src/node_modules/mods/moduleB.ts
   2. /myProject/src/node_modules/mods/moduleB.tsx
   3. /myProject/src/node_modules/mods/moduleB.d.ts
   // etc... (same 8 locations as above)

more complications

before using the search algorithm above, typescript checks tsconfig.json to see if a few special settings were specified:

baseURL

if the baseURL is specified, it will first try to look directly into that path for the module (and not in a node_modules folder under that path, just directly into that path). **Notice that this will make any modules located in that baseURL folder take precedence over node_modules. When you use this, you'll need to make sure your module names do not clash with those in node_modules unless it is your intention to override a node_modules module. Lastly, if the baseURL is specified as a relative path, it will be relative to the location of the tsconfig.json file.

paths

if paths is specified, it will also be checked before the normal module resolution takes place. paths takes all of the modules specified in the import statements and runs them through a transformation algorithm and then checks for the existence of the module at the transformed path location. For example:

   /*
     in this example, the importing module is: /myProject/src/moduleA.ts
     and tsconfig.json is here: /myProject/tsconfig.json
     and moduleB is actually located here: /myLibrary/moduleB.ts
   */
   import * as modB from 'myLib/moduleB';

   // tsconfig.json contains this
   "paths": {
     "myLib/*": ["../myLibrary/*"]    // "myLib/*" is called the "matcher", and the array contains "mapped paths"
   }

   /*
   typescript will first take the path 'myLib/moduleB' and transform it in the following way:
   First, it checks to see if the path matches any of the "matcher" entries.  Since "myLib/*" matches 'myLib/moduleB', the one path associated with it "../myLibrary/*" will be used to transform the module.  It will take the text matched by the * in 'myLib/*' and replace the * in the "mapped path" with it.  When finished, the transformed module path would be:  ../myLibrary/moduleB
   Since this transformed path is relative, the location of the tsconfig.json is used to resolve that. Had a baseURL been specified however, that baseURL location would have been used to resolve this path.
   So, the final path will become /myLibrary/moduleB.ts
   */

after all of the mapping, if a module wasn't found, the normal search algorithm first specified above will occur.

namespace collisions

for large applications, using the "paths" option is an excellent way to prevent horrible module specifiers that look like:

   import * as horrible from '../../../../common/some/deep/location/horror'

however, care must be taken to prevent accidental collisions with third party modules you install with npm. Luckily, npm has some rules about how modules should be named and we can use them to deliberately choose paths that are unlikely to collide with public npm modules.

The completely foolproof way: visit npmjs.com and register your own "scope" for yourself. npm considers any package name beginning with an @ symbol to refer to a registered "scope" which is a unique namespace owned by an npm user. For example Angular uses the prefix @angular/... for all of its module names. Your "scope" for npm will be an @ followed by your user name. If you begin all modules with your registered scope, they will never collide with any public npm packages.

A simpler way--with some small risk of collision: use characters that will never appear in npm package names. For example, npm does not allow the following for package names:

  1. beginning with an underscore: _not_allowed
  2. containing capital letters: notAllowed
  3. containing these characters: ! ~ ( )

Note that this is not 100% foolproof because when npm first began it did not have any restrictions on package names so there are some legacy packages that exist with capital letters and special characters. For example, there seems to be only two packages in npm that slipped in early with underscores called "_design/app" and "_design/scratch", so those are taken, but there will be no new packages added that begin with an underscore. The characters in item #3 above seem to be fine also, as there are currently no package names that begin with those characters and any future attempts to add package names with those characters will be disallowed by npm.

exported variables

become live read-only variables in the importing module. When the declaring module changes the value of the variable, the change will also be seen in the importing module. However, the importing module can not change the imported variable directly, instead, the exporting module would have to export a setXXXX() function.

scripts vs. modules

There are two types of .ts files in Typescript: scripts and modules. Modules are denoted by having the following keywords appear anywhere in the code of the file: import, export, require, exports. Scripts are denoted by having none of those keywords appearing in the code of the file. Generally, your app will consist of numerous modules all exporting and importing functions, types, variables, and constants to and from each other. In that way, there are rarely any global functions, types, variables or constants available to your app. However, anything declared in a script will be global to your application as long as the script is included when transpiling either through ///<reference path=""> tags or through files:[],includes:[] and/or the default **/*.ts in your tsconfig.json.

adding to global scope

If you would like to place anything into global scope from inside one of your modules: declare global { var myGlobalVariable: string; }. This is how some libraries, especially testing tools, can magically make their functions available without needing to import them first.

asynchronous programming

promises

(this is just a quick overview, see here for full discussion)

  • instances of Promise<...> classes are returned by various APIs
  • used when an API intends to invoke a single callback in the future (ex. providing the results of fetching data over a network)
  • allows asynchronous programming to have more of a synchronous feel by chaining asynchronous events sequentially
  • the Promise object has 2 main members:
    then(callback(?){})
    catch(callback(e){})
    pass a function to then() to be executed if/when the promise is "fulfilled" (ex. the fetched data is received)
    pass a function to catch() to be executed if/when the promise is "rejected" usually due to an error (ex. the network was down)
    the parameters for the then() callback function are entirely defined by the function offering the promise
    an Error object should be passed to the catch() callback function, but it too is entirely up to the function to do that
  • async/await
    async function myFunc() {}
    functions can be notated as "async" functions
    calling an async function will always return a Promise immediately
    data = await getData();
    inside of an async function, you may call any function that returns a Promise using the await keyword
    this will cause execution to pause in a synchronous manor (but just for that function) and wait for the Promise to be resolved before moving to the next line of code in that function. Note that the function using the await actually returns as soon as it hits the await keyword and allows your app to continue running. However, execution will start again from the point after the await call once the promise finally resolves.

iterators

generators

  • function* myGenerator() {}
    calling this function always returns an Iterator object
    the iterator object has one main function:
    next() which returns this object: {value:any,done:boolean}
    inside the function, the yield keyword is used to return a value instead of returning
    when executing a next(), the main generator function will execute code right up to the yield command
    subsequent calls to next() will begin execution right after the prior yield command
    function* test() {yield 1; yield 2;}
    t = text(); t.next().value; t.next().value; t.next().value;
    above code returns 1, 2, undefined
    for loops
    for (let name in obj) loops over the property names
    for (let item of arr) loops any enumerable object's items: Array, Map Set, etc.
    To iterate object property values: for (let name in obj) console.log(obj[name]);

regular expressions

  • s.match(/<regex>/) - returns only the first match it finds as an array: ['<full match>','<1st cature group>','<2nd cap.>',...]

  • s.match(/<regex>/g) - returns all matches, but no capture groups: ['<full match 1>','<full match 2>',...]

  • Array.from( s.matchAll(/<regex>/g) ) returns all matches with capture groups like so:

     [
       ['<1st full match>','<1st cature group>','<2nd capture group>',...],
       ['<2nd full match>','<1st cature group>','<2nd capture group>',...],
       ...
     ]
    
  • matchAll() is quite new and only available starting from Node v12.0.0. An alternative is: let regEx = new RegExp(/<regex>/,"g"); followed by multiple calls of regEx.exec(<string>); Each call to exec() will return an array object containing the next match with all capture groups and the index of where the match started.

date and time

the built in Date() object/function handles dates and times. It is very powerful, but has a lot of non-intuitive aspects to it. The Date() object always stores the date internally as the UTC date and time.

  1. Don't call the Date() on its own. It returns a useless date in string form. Always use new Date() which creates a new date object set to the local date and time. (but it stores it as a UTC date and time internally)

  2. There are numerous parameters for creating dates, but we'll just stick to these:

   // creates a date for January 2, 2021 at midnight UTC
   d = new Date('2021-01-02T00:00:00.000Z');

   // creates a date for January 2, 2021 at midnight *Local Time*
   d = new Date('2021-01-02T00:00:00.000');

   // creates a date for January 2, 2021 and the current *Local Time*
   d = new Date('2021-01-02');
  1. Be careful when retrieving the parts of dates--there are some land mines!
  • most functions without UTC in the name (but not all) return local time values - double check before using!
  • getDay() and getUTCDay() return the day of week (0=Sun, 6=Sat) not the day of the month!
  • getMonth() and getUTCMonth() is a zero based number where 0=Jan, and 11=Dec!
  • getTime() returns the number of milliseconds passed since Jan 1, 1970 at midnight UTC
  1. To convert a local date string in the locale format into a date, use d = new Date('1/1/2021')

javascript gotchyas!

Watch out for the following bad language features that can be used accidentally and go unnoticed:

bad if statements

  • if (<statement>, <statement> ... , <condition>)
    example:
    if ( colorsMatch(Colors.blue), '#0000ff ' ) { }
    An accidental closing parenthesis was added after blue, when it should have been added after the html color string param. The statement above always returns true, and it's hard to spot the problem,
  • if (<symbol> = <symbol>)
    example:
    if (a = b) { }
    This one is a classic: it assigns b to a and returns the Boolean evaluation of a (or b, because they're both the same value now!)

function miscall

attempting to call a function without parentheses:

function sayWhatever() { console.log('whatever'); }

sayWhatever;

this should really be an error because it serves no purpose. Clearly the developer meant sayWhatever();

misleading substrings

watch out for the two different substring extractors:

  • String.substr(startIndex,count) - straightforward, does what it says (but is unfortunately being deprecated)
  • String.substring(startIndex,endIndexButNotIncluded) - most JS docs on the web say the second index is just the "end index" but fail to emphasize that the character pointed to by that number is NOT included in the resulting string!

promises

this is a powerful, but tricky concept that requires some mental gymnastics to fully comprehend and fully implement in your application. The promise concept seems simple at first and you might be thinking, "it's just a callback, nothing groundbreaking here.", but extra keywords were created in ECMAScript 6 to make promises shine.

old school - callbacks

first, consider this classic (old-school) JavaScript function:

function fetchData(connection:Connection, callback:(data:any)=>void) {
  ...long process...
};

let fetchedData:any;

fetchData(myConnection, (data) =>{
  fetchedData = data;
  console.log(fetchedData);
});
console.log('waiting for data...')

if fetchData() takes a few seconds to actually get the data, this will return:

> waiting for data...     |  <--- notice this is logged first
> {value:'hi!'}

promises - work pretty much the same way

using a promise for the same function would look like this:

function fetchData(connection:Connection):Promise<any>;

you would use it like this:

let fetchedData:any;
fetchData(myConnection).then((data)=>{
  fetchedData = data;
  console.log(fetchedData);
});
console.log('waiting for data...')

just like the classic example, this this will return:

> waiting for data...     |  <--- notice this is logged first
> {value:'hi!'}

if you're curious: look at the then() part of the promise method; you might notice a potential for a bug: What if the data-fetch happens immediately, that is, before the .then() even gets evaluated by the interpreter? Using the old school way, there is no way that could happen because your callback will be in place before the fetchData() even begins. To solve this with promises, there is a special internal mechanism that will automatically call the .then function's callback even if the event already happened. That way, you'll never miss the event, even though you are technically assigning the then a little bit after the fetchData call. (You can even assign the then long after the call if you want, and it would behave like you actually assigned it right after the call. It seems strange, but it might be useful sometimes.)

so, what's the big deal? A promise is a glorified callback--and even worse, it needs that extra internal baggage to prevent missing the then call if the internal event happens too quickly… but now look at this:

the magic of await

wouldn't it be great if we could make asynchronous calls like this?

console.log('waiting for data...');
let fetchedData = await fetchData(myConnection);
console.log('got the data...');
console.log(fetchedData);

your code would pause while fetchData() completes, then fetchedData would get assigned the resulting data and then the rest of the code after it executes---but while the code was waiting for fetchData() to complete, your app's main thread was free and unblocked. Doesn't that make good sense? AND, when you use this for real-world apps, you can chain multiple asynchronous function calls with the simple and straight forward syntax above.

a more complicated example

consider the examples above, with just one additional asynchronous call lookUpData() added to it. See how complicated it gets when using classic javascript? (and what if you had 4 or 5 calls to make in series??)

let resultData:any;

fetchData(myConnection, (fetchedData) => {
  lookUpData(fetchedData, (result) => {
    resultData = result;
    console.log(resultData)
  })
});
console.log('waiting for result...')

promises reduce asynchronous complexity

with Promises (and await) it feels more like normal synchronous code:

console.log('waiting for result...')
let fetchedData = await fetchData(myConnection);
let resultData = await lookUpData(fetchedData);
console.log(resultData);

and error handling works as expected!

let fetchedData;
try {
  fetchedData = await fetchData(myConnection);
} catch(e) {
  fetchedData = { content: 'error: '+e.mesage }
}

using await in the top level of a module

using await it the top level a module is not supported yet. However the following trick can be used to kick off a promise from the top level. The trick is basically instantiating an anonymous async function by surrounding its declaration with parentheses, and then immediately calling it by using the () at the end of the declaration.

(async () => {
  try {
    var text = await whateverYouWant();
    console.log(text);
  } catch (e) {
    // deal with any problems
  }
})();

in the case of node, this of course means that the entry point into your app will execute all of its code and then wait for the promise to return before exiting.

don't overuse await

with the convenience of using await, it may become tempting to overuse it! Keep in mind that a call to await contains some overhead and may cause a 10ms delay in your app's processing. In other words, await is expensive. It's really meant to be used when calling an external process that is expected to possibly take a second or more to complete -- like network communication. Here's an example of where to avoid await: node offers asynchronous file access functions to deal with the local file system via fs/promises. However, under most circumstances their use is not necessary because accessing local files is so quick (less than 1ms for simple operations!) that adding 10ms with the await call would defeat the purpose!

advanced typescript

object oriented factory for immutable classes

sometimes a class needs to offer a method that creates new instances of itself, and when a child class is derrived from said class, that same method should create instances of the child class. This involves 2 parts: typescript and javascript. You'll need to satisfy both parts when doing this--you would think typescript would assume :this, as the return type, but it doesn't, so you need to make sure you add that.

in the example below, increasing the car's speed should create a new instance of car with its speed increased. The same increaseSpeed() function would be available to Bicycle as well. The this return type allows increaseSpeed() to return instances of descendant classes as well.

abstract class Vehicle {
  constructor(public speed:number) {}
  public increaseSpeed(increase:number):this {                    // <-- ':this' forces typescript to realize that the child class is returned
    return new (this.constructor as any)(this.speed + increase);  // <-- 'as any' stops typescript from complaining -- but it's up to you to
  }                                                               //     make sure that sub classes of Vehicle have the same constructor params.
}

class Bicycle extends Vehicle { }

class Car extends Vehicle { }
let car = new Car(10);
let fastCar = car.increaseSpeed(50);
console.log(fastCar instanceof Car); // true  (the class is a Car, not a Vehicle)
other posts
personal internet security
what *is* electron?
firewall ip filtering overview
javascript binary data
practical http caching
modern css concepts
my favorite vscode extensions
try import helper for vscode
node coding handbook
the case for electron.js