Types in JS

How can we add another protection to our JS application?

If you want to know more about typing in JS, I suggest you check out one of my blogs, where I've covered in depth every types in JS( String, Object, Collections)

Typeof vs vs. instance of

typeof

Per the MDN documentation, typeof is a unary operator that returns a string indicating the type of the unevaluated operand.

In the case of string primitives and string objects, typeof returns the following:

const a = "I'm a string primitive";
const b = new String("I'm a String Object");

typeof a; --> returns 'string'
typeof b; --> returns 'object'
💡
Only works with primitive value (string, number , boolean, to be precise)

Instanceof

is a binary operator, accepting an object and a constructor. It returns a boolean indicating whether or not the object has the given constructor in its prototype chain.

const a = "I'm a string primitive";
const b = new String("I'm a String Object");

a instanceof String; --> returns false
b instanceof String; --> returns true

DOM instance of ???

element instanceof EventTarget;
element instanceof Node;
element instanceof Element;
element instanceof HTMLElement;

Undefined and null💡

💡
To check undefined and null values, we have to use the ===

never

type OneOrTheOthers = {
 collapsed:true;
 expanded?:never;
} | {
 collapsed?:never;
 expanded:true;
}

Array and Object

  • Array can be checked using Array.isArray([]);

  • Object can be checked using myvar instanceof Object;

Thumb Rule:

  • For checking primitive types, use typeof

  • Null can be checked as myvar === null

  • Undefined can be checked as myvar === undefined

  • An array can be checked using Array.isArray([])

  • Object can be checked using myvar instanceof Object

  • Constructor column can be utilized in a similar fashion as ({}).constructor or ([]).constructor

Enum in TS

Enums are a way to enumerate the possible values for a type.

💡
Default enum is index based
enum Language {
    English,
    TS,
    Russian
}
💡
Enum are uppercase and singular. Their keys are also uppercase

TypeScript will automatically infer a number as the value for each member of your enum, but you can also set values explicitly.

enum FIELD_CODES  {
  NUMBER,
  TEXT,
  INTEGER,
  DATE,
  DATETIME,
  LINK,
  CALCRESULT_REFERENCE,
  ENTITY_REFERENCE,
  BOOLEAN
};

var FIELD_CODES;
(function (FIELD_CODES) {
    FIELD_CODES[FIELD_CODES["NUMBER"] = 1] = "NUMBER";
    FIELD_CODES[FIELD_CODES["TEXT"] = 2] = "TEXT";
    FIELD_CODES[FIELD_CODES["INTEGER"] = 3] = "INTEGER";
    FIELD_CODES[FIELD_CODES["DATE"] = 4] = "DATE";
    FIELD_CODES[FIELD_CODES["DATETIME"] = 5] = "DATETIME";
    FIELD_CODES[FIELD_CODES["LINK"] = 6] = "LINK";
    FIELD_CODES[FIELD_CODES["CALCRESULT_REFERENCE"] = 7] = "CALCRESULT_REFERENCE";
    FIELD_CODES[FIELD_CODES["ENTITY_REFERENCE"] = 8] = "ENTITY_REFERENCE";
    FIELD_CODES[FIELD_CODES["BOOLEAN"] = 9] = "BOOLEAN";
})(FIELD_CODES || (FIELD_CODES = {}));
;

Enums are a single object in JS

You can mix strings and values but it will behaves differently

💡
With enum as a string it will do nothing, but if its value is a number it will try to convert the number to key as well
enum Color {
Red = '#c10000', Blue = '#007ac1', Pink = 0xc10050, White = 255
}

var Color;
(function (Color) {
    Color["Red"] = "#c10000";
    Color["Blue"] = "#007ac1";
    Color[Color["Pink"] = 12648528] = "Pink";
    Color[Color["White"] = 255] = "White";
})(Color || (Color = {}));

Function overloaded (extra)

Very common on third-party library

Shorthand call signature

type Log = (message:string,userId?:string)=>void;

Full call signature

type Log = {
   (message:string,userId?:string)=>void;
}

Function overloaded

/* 
    This is benifical to whoever use this function from outside
    Get better code's instruction
*/
type Reserve = {
    (from:Date,to:Date,destination:string):Reservation
    (from:Date,destination:string):Reservation
}

/*
    The actual implementation from the function
*/
let reverse : Reserve =
    (from:Date,toOrDestination: Date | string, destination:string)=>{
    // Implementation
    if(toOrDestination instanceof Date && destination !== undefined)
    {
        // two ways
    }
    if(typeof destination === string) {
        // one way
    }
}
💡
When using overload trying to keep your implementation's signature as specific as possible to make it easy to implement the function
interface Document extends Node, DocumentOrShadowRoot, FontFaceSource..{
    createEvent(eventInterface:"AnimationEvent"):AnimationEvent;
    createEvent(eventInterface:"ClipboardEvent"):ClipboardEvent;
    createEvent(eventInterface:"CloseEvent"):CloseEvent;
    .....
}
type Get = {
    <O extends object,K extends keyof O>(object:O,key:K):O[K];
    <O extends object,K extends keyof O,K1 extends keyof O[K]>(object:O,key:K,key2:K1):O[K][K1];
    <O extends object,K extends keyof O,K1 extends keyof O[K],K2 extends keyof O[K][K1]>(object:O,key:K,key1:K1,key2:K2):O[K][K1][K2];
}

let get:Get = (object:any,...keys:string[])=> {
    let result = object;
    keys.forEach((key)=>result= result[key]);
    return result;
}

const obj = {
    a:{
        b:{
            c:"d"
        }
    }
}

const result = get(obj,"a","b","c");

Function overloaded with lodash

interface LodashMap {
    <T,TResult>(iteratee:(value:T)=>TResult):LodashMap1x1<T,TResult>;
    <T,TResult>(iteratee:(value:T)=>TResult,collection:T[]):TResult[];
    <T extends object,TResult>(iteratee:(value:T[keyof T])=>TResult):LodashMap2x1<T,TResult>;
    <T extends object,TResult>(iteratee:(value:T[keyof T])=>TResult,collection:T):TResult[];
    <T extends object,K extends keyof T>(iteratee:K):LodashMap3x1<T,K>;
    <T extends object,K extends keyof T>(iteratee:K,collection:T):Array<T[K]>;
    (iteratee:string):LodashMap4x1;
}

type LodashMap1x1<T,TResult> = (collection:T[])=>TResult[];
type LodashMap2x1<T,TResult> = (collection:T)=>TResult[];
type LodashMap3x1<T,K extends keyof T>=(collection:T)=>Array<T[K]>;
type LodashMap4x1 = <T>(collection:T) => any[];
function iterators(value:number):number {
     return value * 2;
}
const mapDouble = map(iterators);
const result = mapDouble([1,2,3]);

Type coercion

1 == "1"

// True

In simple term, type coersion means a language converts a certain type to a certain type

Do all languages have type coercion?

Yes, we do, because we need to transform some types into others. Even when we write some JS behind the scenes, it translates to a bunch of 0 and 1 :) JS is a special language that, by nature, is a little bit too much

== (Compare two different values, if they have different types
translate it to the same type)
=== (Compare two values but don't translate it please)

In JS world, the standard of the industry is to always use ===
because == is very blur

If statement with coersion

if(1) {
   console.log(5);
}
This is so tricky, JS coersion this to boolean because it
guess that in if else statement we always wants to have boolean value
💡
This is so confusing and is mainly the main bug in our application

💡
I would say that if else coersion in JS works really well with primitive value("",0,false,null,undefined) but not with objects. This is exactly how the lodash compact function works

There're some quirks with if-else statement I'd like to point out

  • [] is not a true value

  • {} is not a true value

The in type guard

The in type guard checks if an object has a particular property, using that to differentiate between different types. It usually returns a boolean, which indicates if the property exists in that object. It is used for its narrowing features as well as to check for browser support.

The basic syntax for the in type guard is below

"house" in { name: "test", house: { parts: "door" } }; // => true
"house" in { name: "test", house: { parts: "windows" } }; // => true
"house" in { name: "test", house: { parts: "roof" } }; // => true
"house" in { name: "test" }; // => false
"house" in { name: "test", house: undefined }; // => true

Object.prototype.hasOwnProperty()

const obj = {
  a: 1
};
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('toString')); // false, because `toString` is inherited from Object.prototype
if(obj[key]) {
    // The value of object is truthy, doesn't mean that object has a key
}

const obj = {
  a: 0,
  b: false,
  c: null,
};
console.log(obj['a']); // 0
console.log(obj['b']); // false
console.log(obj['c']); // null

if (obj['a']) {
  console.log('a exists and is truthy'); // This won't execute because 0 is falsy.
}

if (obj.hasOwnProperty('a')) {
  console.log('a exists'); // This will execute because 'a' is a property on `obj`, even though its value is 0.
}

Nullish coalescing operator?? (ES2020)

💡
Nullist = undefined || null;

let myPower = {
    mind : {
       level:"";
    }
}

let mindPower = myPower?.mind?.level || "no power"; // no power

// What if we want to only check for undefined and null
mindPower = myPower?.mind?.level ?? "no power"

typeof in TS

//Bassed on the context
type T1 = typeof p; // type is Person
type T2 = typeof email; // type is (p:Person,subject:string)=>Response

const v1 = typeof p; // value is an "object"
const v2 = typeof email; // value is "function"
💡
TS type is much more complicated and oftentimes provides what we want

Any vs unknown

In general, it's recommended to use 'unknown' instead of 'any' whenever possible, because it helps to catch type errors at compile time and ensures that your code is type-safe.

type AdminUser = {
    data:string;
}

// Runtime JS + Static analysis with TS
function isAdminUser(arg:any)arg is AdminUser{
      if(arg instanceof Object) {
           return "data" in arg;
      }
      return false;
}

//with promise after we run .json() it returns any
const goodUser:unknown = await response.json();
//if we don't type-guard it, there's a chance that it will lead to error
if(isAdminUser(goodUser) {
    goodUser.data; //could be error if server returns undefined;
}

User-defined Defined guards

As we know, JS has type guard checking like we listed above, but it's too simple in real life when we work with complicated data. For those cases, we can define type guard functions. These are just functions that return someArgumentName is SomeType

type Foo = {
    a:string;
}

// Open the gate as wide as possible with any, close the gate with is
function isFoo(arg:any): arg is Foo {
    return (typeof arg.a === "string" && arg.a!=="");
}

function doStuff(arg?:Foo){ 
    if(isFoo(arg)) {
        //do something
    }
}
💡
Type-guard is especially useful when we're dealing with data from a server. Sometimes it's there, sometimes it's not there, so best practice is to make the assumption that if it's not there, we should handle it in different way

Type-checking at runtime

//Type's checking functions
function isITeam(arg:any):arg is Iteam {
    return (
        typeof arg.iconUrl === "string" &&
        typeof arg.name === "string" &&
        typeof arg.id === "string" &&
        Array.isArray(arg.channels);
    )
}

//High-order-functions
function assertIsTypedArray<T>(arg:any,
         check:(val:any)=>assert arg is T[]) {
    if(!Array.isArray(val) {
        throw new Error('Not an array');
    }
    if(arg.some((item)=>!check(item)) {
        throw new Error('Violators found');
    }
}

// Use-case
cachedAllItems = apiCall('teams').then((rawData)=> {
     assertIsTypedArray<Iteam>(rawData,isITeam);
     return rawData;
})

Type narrowing

We can narrow the type from a broad type to a narrower one to handle some specific logics depending on its type

const el = document.getElementById('foo'); null || HTMLElement
if(!el) {
    el.innerHTML = "Party Time!";
} else {
    alert('No elements was found');
}

Narrowing types

Instanceof

function contains(text:string, search:string | RegExp) {
    if(search instanceof RegExp) 
        { search.exec(text); }
    text.include(search);
}

In

interface A = { x:number }
interface B = { y:number }
function pickAB(ab: A| B ) {
    if("x" in ab) { return ab.x }
    if("y" in ab) { return ab.y }
}

Array.isArray

function contains(text:string, terms:string | Array<string>){
    if(Array.isArray(terms) { term.map(..) //Array methods }
    terms.startWith('H');
}

Be-careful with null

const el = document.getElementById("foo"); //HTMLElement | null
if(typeof el === "object") {
     //null is an object
}


function foo(x?:number | string | null) {
    if(!x) {
        x; //Type is string | number | null | undefined
    }
}

False primitive values

function foo(x?:number | string | null) {
    if(!x) {
        x; //Type is string | number | null | undefined
    }
}
//because "" and 0 are both falsy values

User-defined-type-guard for DOM

Like mentioned above, with complicated types, we can create our own function to do the checking for us :)

function isInputElement(element:any):element is HTMLInputElement {
   return 'value' in element;
   //Smart move only value exists in HTMLInput element
}

function getElementContent(el:HTMLElement) {
  if(isInputElement(element)) {
      el.value; // HTMLInputElement;
  } 
  else {
      el.textContent; // HTMLElement
  }
}