Photo by Florian Klauer on Unsplash
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'
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💡
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.
enum Language {
English,
TS,
Russian
}
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
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
}
}
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
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)
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"
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-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
}
}