How to generate getters and setters in Javascript? - javascript

I have the following code, which is very repetitious:
const flags = {
get logged() {
return localStorage.getItem("logged") === "true";
},
set logged(val: boolean) {
if (val) {
localStorage.setItem("logged", "true");
} else {
localStorage.removeItem("logged");
}
},
get notificationsMuted() {
return localStorage.getItem("notifications-muted") === "true";
},
set notificationsMuted(val: boolean) {
if (val) {
localStorage.setItem("notifications-muted", "true");
} else {
localStorage.removeItem("notifications-muted");
}
}
}
As you can see, the get and set for each flag type is identical, save for the property names. I would like to do something like this instead:
function getter(prop: string) {
return localStorage.getItem(prop) === "true";
}
function setter(prop: string, val: boolean) {
if (val) {
localStorage.setItem(prop, "true");
} else {
localStorage.removeItem(prop);
}
}
const flags = {
get logged: getter("logged")
set logged: setter("logged")
get notificationsMuted: getter("notifications-muted")
set notificationsMuted: setter("notifications-muted")
}
But I'm not sure if Javascript / Typescript has support for this sort of thing. Is such a thing possible, and if so, how? If not, is there any other way I can cut down on the repetition here?

You can use a proxy with get and set traps, use TS types to allow only props you wish to handle (TS playground)):
interface Flags {
logged: boolean,
'notifications-muted': boolean;
}
type Prop = keyof Flags;
const handlers = {
get(_: Flags, prop: Prop) {
return localStorage.getItem(prop) === "true";
},
set(_: Flags, prop: Prop, val: any) {
if (val) {
localStorage.setItem(prop, "true");
} else {
localStorage.removeItem(prop);
}
return true;
}
};
const flags = new Proxy<Flags>({} as Flags, handlers);

All you really need is to use Object.defineProperty with an object with a get and set properties. Or, with multiple properties, use Object.defineProperties to define them all at once.
One approach which will help with code organization is to not use lots of local storage keys, but instead use a single object that gets stored.
const props = ['logged', 'notificationsMuted'] as const;
const defaultStorage = Object.fromEntries(props.map(prop => [prop, false]));
const getStorage = () => JSON.parse(localStorage.getItem('settings') || JSON.stringify(defaultStorage));
const flags = Object.defineProperties(
{},
Object.fromEntries(
props.map(
prop => [
prop,
{
get: () => getStorage()[prop],
set: (newVal: boolean) => {
const store = getStorage();
store.prop = newVal;
localStorage.setItem('settings', JSON.stringify(store));
}
}
]
)
)
) as Record<(typeof props)[number], boolean>;

This is the current "best" solution I can come up with. Open to anyone who can provide an improvement over this:
function getter(prop: string): boolean {
return localStorage.getItem(prop) === "true";
}
function setter(prop: string, val: boolean): void {
if (val) {
localStorage.setItem(prop, "true");
} else {
localStorage.removeItem(prop);
}
}
const flags = {
get logged() { return getter("logged") },
set logged(val: boolean) { setter("logged", val) },
get notificationsMuted() { return getter("notifications-muted"); },
set notificationsMuted(val: boolean) { setter("notifications-muted", val); }
}

Related

set name of properties dynamically JavaScript [duplicate]

Is there a way in typescript to set a property name from a variable?
Something like this
export function objectFactory(prop: string) {
return {
prop: {
valid: false
}
};
}
You are looking for computed properties, this is an ES6 feature and not specific to TypeScript.
export function objectFactory(prop: string) {
return {
[prop]: {
valid: false
}
};
}
You can do it like this:
export function objectFactory(prop: string) {
let data: any = {};
data[prop] = {};
data[prop].valid = false;
return data;
}

How to assign values to object that has similar keys?

just wanted to avoid if statements is there way to assign values to object if they have similar keys in response instead of checking each object with if ?
what could be efficient approach here ?
main.ts
public responsehandler(#Body()data: any): any {
const response: Idetails = {} as Idetails;
if (data.details === undefined || data.details === null) {
return data;
}
if (data.details) {
if (data.details.primary) {
response.details.primary.beginningBalance = data.details.primary.beginningBalance;
response.details.primary.endingBalance = data.details.primary.endingBalance;
}
if (data.details.secondary) {
response.details.secondary.beginningBalance = data.details.secondary.beginningBalance;
response.details.secondary.endingBalance = data.details.secondary.endingBalance;
}
}
return response;
}
interface.ts
export interface Idetails {
primary:balanceDetails;
secondary: balanceDetails;
}
export interface balanceDetails {
beginningBalance: string;
endingBalance: string;
}
data
details: {
primary: {
beginningBalance: 458,
endingBalance: 890
},
secondary: {
beginningBalance: 47,
endingBalance: 871
}
}
Check this out: https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Objetos_globales/Object/hasOwnProperty
You could try this approach:
public responsehandler(#Body()data: any): any {
const response: Idetails = {} as Idetails;
if (!data.details) {
return data;
}
for (const key in response.details) {
if (data.details.hasOwnProperty(key)) {
response.details[key] = data.details[key];
}
}
return response;
}
You should check more validations or conditions to make it work for you.
If your goal is just to avoid if statements, you could rewrite it like so:
// in js (sorry not a ts user)
function responseHandler(data) {
return data.details == null ? data : {details: {...data.details}};
}
console.log(responseHandler({details: {primary: {beginningBalance: 0, endingBalance: 1}}}));
console.log(responseHandler({details: {secondary: {beginningBalance: 0, endingBalance: 1}}}));
console.log(responseHandler({noDetails: 'oopsy'}));

JS Design Pattern for setting a var set for functions

I am working on a react project but I think this question relates to all JS
I have a Token file that contains the following functions:
export default {
set(token) {
Cookies.set(key, token);
return localStorage.setItem(key, token);
},
clear() {
Cookies.remove(key);
return localStorage.removeItem(key);
},
get() {
try {
const retVal = localStorage.getItem(key) || '';
return retVal;
} catch (e) {
return '';
}
},
Now I want to add a set of what are essentially environment variables for the domain of these 3 functions. In my case its based on window.location.hostname but could really be anything.
In this instance lets say we want key to be dev, uat or prod based on window.location.hostname
getKey = host => {
if(host === 'a')
return 'dev'
elseIf (host === 'b')
return 'uat'
else
return 'prod'
}
I think the above is fairly standard to return the key you want. but what if your key has 6 vars, or 8, or 20. How could you set all the vars so that when you call set(), clear() and get() that they have access to them?
Basically I want to wrap the export in a function that sets some vars?
To illustrate this a bit more
class session extends Repo {
static state = {
current: false,
};
current(bool) {
this.state.current = bool;
return this;
}
query(values) {
<Sequelize Query>
});
}
export session = new Session();
using this I can call current(true).session() and sessions state would be set to true. I want to apply a similar pattern to the Token file but I don't want to change all my calls from Token.set(token) to Token.env(hostname).set(token)
This acomplished what I wanted, I had to call the function from within the others as window is not available on load. It essentially illustrates the pattern I was looking for. Thanks for Jim Jeffries for pointing me towards the answer.
class Token {
constructor(props) {
this.state = {
testKey: null,
};
}
setCred = host => {
if (host === 'uat') {
this.state.testKey = 'uat';
} else if (host === 'prod') {
this.state.testKey = 'prod';
} else {
this.state.testKey = 'dev';
}
};
set(token) {
this.setCred(window.location.hostname);
Cookies.set(testKey, token);
return localStorage.setItem(testKey, token);
}
clear() {
this.setCred(window.location.hostname);
Cookies.remove(testKey);
return localStorage.removeItem(testKey);
}
get() {
this.setCred(window.location.hostname);
try {
const retVal = localStorage.getItem(key) || '';
return retVal;
} catch (e) {
return '';
}
}
}
export default new Token();
If anyone else has another idea please share.

Property name on object from variable

Is there a way in typescript to set a property name from a variable?
Something like this
export function objectFactory(prop: string) {
return {
prop: {
valid: false
}
};
}
You are looking for computed properties, this is an ES6 feature and not specific to TypeScript.
export function objectFactory(prop: string) {
return {
[prop]: {
valid: false
}
};
}
You can do it like this:
export function objectFactory(prop: string) {
let data: any = {};
data[prop] = {};
data[prop].valid = false;
return data;
}

Map Typescript Enum

How would I map a typescript enum? For example, with strings you can do this:
let arr = [ 'Hello', 'Goodbye' ];
arr.map(v => {
if (v === 'Hello') {
return ':)';
} else if (v === 'Goodbye') {
return ':(';
}
); // [ ':)', ':(' ]
This, of course, doesn't work with enums:
enum MyEnum { Hello, Goodbye };
MyEnum.map(v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // does not work
Ideally, I'd like to do this in a generalized way so I can simply take any enum I have and put it through a map function while preserving type information. Usage might look something like this:
map(MyEnum, v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // [ ':)', ':(' ]
I've been fiddling around with getting a function that does this for me but keep having issues getting the generics just right.
To map an enum do this:
(Object.keys(MyEnum) as Array<keyof typeof MyEnum>).map((key) => {})
The function to solve this is quite simple.
// you can't use "enum" as a type, so use this.
type EnumType = { [s: number]: string };
function mapEnum (enumerable: EnumType, fn: Function): any[] {
// get all the members of the enum
let enumMembers: any[] = Object.keys(enumerable).map(key => enumerable[key]);
// we are only interested in the numeric identifiers as these represent the values
let enumValues: number[] = enumMembers.filter(v => typeof v === "number");
// now map through the enum values
return enumValues.map(m => fn(m));
}
As you can see, we first need to get all of the keys for the enum (MyEnum.Hello is actually 1 at runtime) and then just map through those, passing the function on.
Using it is also simple (identical to your example, although I changed the name):
enum MyEnum { Hello, Goodbye };
let results = mapEnum(MyEnum, v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
});
console.log(results); // [ ':)', ':(' ]
The reason we need to filter the enum to be numbers only is because of the way enums are compiled.
Your enum is actually compiled to this:
var MyEnum;
(function (MyEnum) {
MyEnum[MyEnum["Hello"] = 0] = "Hello";
MyEnum[MyEnum["Goodbye"] = 1] = "Goodbye";
})(MyEnum || (MyEnum = {}));
;
However we are not interested in "Hello" or "Goodbye" as we can't use those at runtime.
You will also notice a funny type statement right before the function. This is because you can't type a parameter as someParameter: enum, you need to explicitly state it as a number -> string map.
Mapping in Typescript can be extremely powerful for writing less code.
I have been using key value Enum mapping a lot recently and would recommend it!
Here are a couple of examples!
Basic enum usage
enum InlineStyle {
"Bold",
"Italic",
"Underline"
}
type IS = keyof typeof InlineStyle
// Example of looping
(Object.keys(InlineStyle) as Array<IS>).forEach((key) => {
// code here
})
// Example of calling a function
const styleInline = (style: IS) => {
// code here
}
Enum key value usage
enum ListStyle {
"UL" = "List",
"OL" = "Bullet points"
}
// Example of looping
Object.entries(ListStyle).forEach(([key, value]) => {
// code here
})
Interface mapping
enum InlineStyle {
"Bold" = "isBold",
"Italic" = "isItalic",
"Underline" = "isUnderlined"
}
type InlineStyleType = Record<InlineStyle, boolean>
enum ListStyle {
"UL",
"OL"
}
type LS keyof typeof ListStyle
interface HTMLBlock extends InlineStyleType {
// This has extended with
// isBold: boolean
// isItalic: boolean
// isUnderlined: boolean
listType: LS
}
With ts-enum-util (npm, github), it's easy, type-safe (uses generics), and takes care of skipping the numeric reverse lookup entries for you:
import { $enum } from "ts-enum-util";
enum MyEnum { Hello, Goodbye };
$enum(MyEnum).map(v => {
if (v === MyEnum.Hello) {
return ':)';
} else if (v === MyEnum.Goodbye) {
return ':(';
}
}); // produces [':(', ':)']
NOTE: ts-enum-util always iterates based on the order of the sorted enum keys to guarantee consistent order in all environments. Object.keys() does not have a guaranteed order, so it's impossible to iterate enums "in the order they were defined" in a cross-platform guaranteed way.
(update: new version of ts-enum-util now preserves the original order in which the enum was defined)
If you are using string enums, then combine it with ts-string-visitor (npm, github) for even more generic type-safe compiler checks to guarantee that you handle all possible enum values in your map function:
(update: new version of ts-enum-util now includes functionality of ts-string-visitor, and it works on numeric enums now too!)
import { $enum } from "ts-enum-util";
import { mapString } from "ts-string-visitor";
enum MyEnum { Hello = "HELLO", Goodbye = "GOODBYE" };
$enum(MyEnum).map(v => {
// compiler error if you forget to handle a value, or if you
// refactor the enum to have different values, etc.
return mapString(v).with({
[MyEnum.Hello]: ':)',
[MyEnum.Goodby]: ':('
});
}); // produces [':(', ':)']
I would not call it general but I use this many times and may it will be handy for others too:
type TMyEnum = ':)'|':(';
class MyEnum {
static Hello: TMyEnum = ':)';
static Goodbye: TMyEnum = ':(';
}
console.log(MyEnum.Hello); // :)
console.log(MyEnum.Goodbye); // :(
Now you don't need any mapping function and it works as expected however you have to create separate similar class for every enum (which should not be a problem since you would do at anyway). The only drawback I can think now is that you can not iterate over it's properties. But until now it wasn't a problem for me I didn't need it. And you can add a static array to the class when you need it.
Maybe this will help you:
enum NumericEnums {
'PARAM1' = 1,
'PARAM2',
'PARAM3',
}
enum HeterogeneousEnums {
PARAM1 = 'First',
PARAM2 = 'Second',
PARAM3 = 3,
}
type EnumType = { [key: string]: string | number };
type EnumAsArrayType = {
key: string;
value: string | number;
}[];
const enumToArray = (data: EnumType): EnumAsArrayType =>
Object.keys(data)
.filter((key) => Number.isNaN(+key))
.map((key: string) => ({
key,
value: data[key],
}));
console.log(enumToArray(NumericEnums));
console.log(enumToArray(HeterogeneousEnums));
// Usage
enumToArray(HeterogeneousEnums).map(({ key, value }) => {
console.log(`${key}: ${value}`);
// Your necessary logic
return null;
});
Console result
This is a working function you can use. Below I'm passing ItemMaterial to getEnumKeys function and getting ["YELLOW", "WHITE", "ROSE", "BLACK"].
Similarly use the getEnumValues function to get values of the enum.
Take a look at the splitEnumKeysAndValues function to see how these variables extracted from the enum.
enum ItemMaterial {
YELLOW,
WHITE,
ROSE,
BLACK,
}
const keys = getEnumKeys<typeof ItemMaterial>(ItemMaterial)
const values = getEnumValues<typeof ItemMaterial, `${ItemMaterial}`>(ItemMaterial);
function getEnumKeys<TypeofEnum>(value: TypeofEnum): keyof TypeofEnum {
const { values, keys } = splitEnumKeysAndValues(value);
return keys as unknown as keyof TypeofEnum;
}
function getEnumValues<TypeofEnum, PossibleValues>(value: TypeofEnum): PossibleValues[] {
const { values, keys } = splitEnumKeysAndValues(value);
return values as unknown as PossibleValues[];
}
function splitEnumKeysAndValues<T>(value: T): { keys: keyof T, values: Array<string | number> } {
const enumKeys = Object.keys(value);
const indexToSplit = enumKeys.length / 2
const enumKeysKeyNames = enumKeys.slice(0, indexToSplit) as unknown as keyof T;
const enumKeysKeyValues = enumKeys.slice(indexToSplit);
return {
keys: enumKeysKeyNames,
values: enumKeysKeyValues,
}
}

Categories

Resources