Is there a way, in javascript (no typescript), to specify that the parameter of a method has to be "one of" [value1, value2]?
For example, if I have a function:
const handleCommentAction = (action) => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
}
}
if there any way to specify that action has to be one of ["add", "delete"] ?
// Something like this...
const handleCommentAction = (action: ["add", "delete"]) => {
or is impossible?
It can be enforced at runtime only by throwing an error or something:
const handleCommentAction = (action) => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
} else {
throw new Error('wrong parameter');
}
}
A better solution would be to use JSDoc to indicate that the argument must be of a particular type:
/**
* #param {'add' | 'delete'} action - The action to perform
*/
const handleCommentAction = (action) => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
}
};
But this is only documentation, not a requirement of consumers of handleCommentAction.
A more elaborate solution (definitely worth it in larger projects, but arguably overkill for small scripts) would be to use TypeScript or some other type-aware system:
const handleCommentAction = (action: 'add' | 'delete') => {
if (action === "add") {
setTotalComments(totalComments + 1);
} else if (action === "delete") {
setTotalComments(totalComments - 1);
}
};
Related
I'm learning JavaScript, and I've done this code, I just want to be sure that I'm on the right way, would you have done it differently?
What bothers me is especially to have passed parameters to the functions "next" and "updateArrows" to know the origin of the clicks
const changeStep = document.querySelectorAll(".step");
const currentPaginate = document.querySelector(".pagination span.active");
const arrows = document.querySelectorAll(".arrow");
for (let arrow of arrows) {
arrow.addEventListener("click", function () {
updateArrows(arrow);
});
}
for (let step of changeStep) {
step.addEventListener("click", function () {
next(step);
});
}
function updateArrows(arrow, currentStep = null, update = true) {
let nextStep;
if (currentStep == null) {
currentStep = document.querySelector(".step.current");
if (arrow.classList.contains("arrow-bottom")) {
nextStep = currentStep.nextElementSibling;
} else {
nextStep = currentStep.previousElementSibling;
}
} else nextStep = document.querySelector(".step.current");
if (!arrow.classList.contains("impossible")) {
if (nextStep.dataset.id != 1 && nextStep.dataset.id != 5) {
arrows.forEach(function (arrow) {
if (arrow.classList.contains("impossible")) {
arrow.classList.remove("impossible");
}
});
} else if (nextStep.dataset.id == 5) {
if (arrow.previousElementSibling.classList.contains("impossible"))
arrow.previousElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
} else if (nextStep.dataset.id == 1) {
if (arrow.nextElementSibling.classList.contains("impossible"))
arrow.nextElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
}
if (update == true) next(nextStep, false);
}
}
function next(step, update = true) {
if (!step.classList.contains("current")) {
const currentStep = document.querySelector(".step.current");
const nextStep = step.dataset.id;
currentStep.classList.remove("current");
step.classList.add("current");
currentPaginate.textContent = "0" + nextStep;
let arrow;
if (currentStep.dataset.id < nextStep) {
arrow = document.querySelector(".arrow-bottom");
} else {
arrow = document.querySelector(".arrow-top");
}
if (update == true) updateArrows(arrow, currentStep, false);
}
}
I see what you mean.
Yes you are right. You can do it better...
Instead of passing the parameter arrow you can read from an event object
const changeStep = document.querySelectorAll(".step");
const currentPaginate = document.querySelector(".pagination span.active");
const arrows = document.querySelectorAll(".arrow");
for (let arrow of arrows) arrow.addEventListener("click", updateArrows);
for (let step of changeStep) step.addEventListener("click", next);
function updateArrows(e, currentStep = null, update = true) {
let arrow = null
e.target ? arrow=e.target : arrow=e
let nextStep;
if (currentStep == null) {
currentStep = document.querySelector(".step.current");
if (arrow.classList.contains("arrow-bottom")) {
nextStep = currentStep.nextElementSibling;
} else {
nextStep = currentStep.previousElementSibling;
}
} else nextStep = document.querySelector(".step.current");
if (!arrow.classList.contains("impossible")) {
if (nextStep.dataset.id != 1 && nextStep.dataset.id != 5) {
arrows.forEach(function (arrow) {
if (arrow.classList.contains("impossible")) {
arrow.classList.remove("impossible");
}
});
} else if (nextStep.dataset.id == 5) {
if (arrow.previousElementSibling.classList.contains("impossible"))
arrow.previousElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
} else if (nextStep.dataset.id == 1) {
if (arrow.nextElementSibling.classList.contains("impossible"))
arrow.nextElementSibling.classList.remove("impossible");
arrow.classList.add("impossible");
}
if (update == true) next(nextStep, false);
}
}
function next(e, update = true) {
let step = null
e.target ? step = e.target : step=e
if (!step.classList.contains("current")) {
const currentStep = document.querySelector(".step.current");
const nextStep = step.dataset.id;
currentStep.classList.remove("current");
step.classList.add("current");
currentPaginate.textContent = "0" + nextStep;
let arrow;
if (currentStep.dataset.id < nextStep) {
arrow = document.querySelector(".arrow-bottom");
} else {
arrow = document.querySelector(".arrow-top");
}
if (update == true) updateArrows(arrow, currentStep, false);
}
}
This should work, if not please contact me beforehand.
While activating the eventListener an event object is passed to function and e.target is an element which was clicked in this case.
What I did was crucial because you sometimes call this function from an eventListener and sometimes from a code. If the element has e.target then it's from an eventListener and if not then it's from code.
Didn't have a chance to test it since I don't have the rest of the code. Let me know if it works.
Nested flow
I need some help design nested multistep form. I have implemented it with a naive solution for tracking the step number, looking for a way to abstract and improve the architecture.
Currently, I have two states the currentStep and the currentSubStep. These are the handlers for back and continue.
const handleNext = () => {
const subSteps = steps?.[currentStep]?.subSteps;
if (currentStep === steps?.length - 1) {
return;
}
if (currentSubStep === subSteps?.length - 1) {
// move to the next step
setCurrentStep((prev) => prev + 1);
setCurrentSubStep(0);
} else {
// move to the next
setCurrentSubStep((prev) => prev + 1);
}
};
const handleBack = () => {
const subSteps = steps?.[currentStep]?.subSteps;
if (currentStep === 0) {
if (currentSubStep !== 0) {
setCurrentSubStep((prev) => prev - 1);
} else if (currentSubStep === 0) {
setCurrentSubStep(0);
} else {
setCurrentStep((prev) => prev - 1);
}
} else {
if (currentSubStep !== 0) {
setCurrentSubStep((prev) => prev - 1);
} else {
setCurrentStep((prev) => prev - 1);
setCurrentSubStep(subSteps?.length - 1 || 0);
}
}
};
I have lots of conditions and if I wrote it with if .. else it works fine but may be hard to read for others (especially if it will grow in future). Is there any better way how to rewrite it in more readable way?
My code:
func(el: IHeadlines): boolean => {
if (el.type === 'Cars' && el.label) { return true; }
if (el.type === 'Bikes' && el.storage) {
if (el.storage.filter(el => el.id === 1).length >= 1) { return true; }
if (el.storage.filter(el => el.id === 2).length > 1) { return true; }
} else return false;
}
interface IHeadlines {
type: string;
label: string;
storage: [{id: number; name: string}]
}
If you ask me, I create a function that counts the value and the code will be much more readable.
function count(arr, tar) {
cnt = 0;
for(let val of arr) {
if(val === tar)
cnt += 1;
}
return cnt;
}
You can write your function this way, this is way more readable to me:
const func = (el) => {
if (el.type === 'Cars' && el.label)
return true;
if (el.type === 'Bikes' && el.storage)
if(count(el.storage,1) >= 1 || count(el.storage,2) > 1)
return true;
return false;
}
Or this way:
const func = (el) => {
if (el.type === 'Cars' && el.label)
return true;
if ((el.type === 'Bikes' && el.storage) &&
(count(el.storage,1) >= 1 || count(el.storage,2) > 1))
return true;
return false;
For this is much more readable, however you can change the count function to any other way you like, and I would prefer using this code even if it is longer but it is much more readable.
Have you tried using switch-case?
for example:
function(el)=>{
switch(el.type):
case 'Cars':
return true;
break;
case 'Bikes':
return true;
break;
default:
return false;
}
After this, you can perhaps put if-else before 'return' in each case.
hello, i do some optimize. I wish this can help you.
const oldFunc = (el) => {
if (el.type === 'News' && el.label) {
return true;
}
if (el.type === 'Research' && el.storage) {
if (el.storage.filter(el => el.id === 1).length >= 1) {
return true;
}
if (el.storage.filter(el => el.id === 2).length > 1) {
return true;
}
} else return false;
}
// do some optimize
const newFunc = (el) => {
let flag = false;// default return false
flag = ((el.type === 'News' && el.label) ||
((el.type === 'Research' && el.hasOwnProperty('storage')) ? el.storage.some(o=>[1,2].includes(o.id)): false)) && true;
return flag;
}
// test code
const testData = {
type: 'News',
label: 'test'
};
console.log(oldFunc(testData));
console.log(newFunc(testData));
const testData2 = {
type: 'Research',
storage: [
{
id: 1,
name: "John"
}
]
};
console.log(oldFunc(testData2));
console.log(newFunc(testData2));
// test result
// true
// true
// true
// true
Two ways come to my mind, but none of them will make your code very clear because conditions are dirty stuff.
If you return boolean, you don't need if..else blocks. Just return the conditions.
func(el) => {
return (el.type === 'Cars' && el.label) ||
(el.type === 'Bikes' &&
(el.storage?.filter(el => el.id == 1).length >= 1 ||
el.storage?.filter(el => el.id == 2).length > 1)
)
}
You can extract the group of conditions to separate functions and call them in the main function.
const checkForCars = (el) => { return el.type === 'Cars' && el.label }
const checkForBikes = (el) => { return // your conditions}
const mainFunction (el) {
return checkForCars(el) || checkForBikes(el);
}
I generally try to be as descriptive as possible. Rename func to what the function does.
For readability, you could also create a function in el called, isCar(), isBike(), hasStorage() etc etc which would encapsulate that logic. I'm not sure if that makes sense based on what you provided. You are also inline hard coding ids. It would make it clearer if el contained some const / var or something in your app had them, which described the id. You could also rename el to something descriptive. You can remove some if by doing what Guerric P said.
Even the filter functions could be moved if they were going to be reused..
const shedFilter = (el) => ...(function code here)
Then provide some comments if anything is not clear.
const STORAGE_SHED = 1;
const SOTRAGE_GARAGE = 2;
aGoodName(el) => {
if (el.isCar()) { return true; }
if (el.isBike()) {
if (el.storage.filter(el => el.id === STORAGE_SHED ).length >= 1) { return true; }
if (el.storage.filter(el => el.id === SOTRAGE_GARAGE).length > 1) { return true; }
}
else { return false };
}
Step 1: To have exactly same logic as you had - you can start with extracting the conditions/function and trying to avoid return true and return false statements. You can easily return condition itself.
const isNews = el.type === 'News' && el.label;
const isBikes = el.type === 'Bikes' && el.storage;
const storageItemsCount = (el, id) => el.storage.filter(el.id === id).length;
return isNews
|| (isBikes && (storageItemsCount(el, 1) >= 1 || storageItemsCount(el, 2) > 1)
Step 2: further I would remove "magic" id 1 and 2 values and explicitly specify what they are about, like
const BIKE1_ID = 1;
const BIKE2_ID = 2;
no we can generalise the counts check with specifying
const minBikeCounts = {
[BIKE1_ID]: 1,
[BIKE1_ID]: 2
}
const bikeCountsAreValid = el => {
return Object.entries(k)
.every(
([id, minCount]) => el.storage.filter(el => el.id === key).length >= minCount)
}
so the main flow simplified to
const isNews = el.type === 'News' && el.label;
const isBikes = el.type === 'Bikes' && el.storage;
return isNews || (isBikes && bikeCountsAreValid(el))
Step 3: We can see a pattern of "Check if type is supported with some extra check". We could extract the knowledge of supported types to separate structure. Now if we want to add new supported type we don't need to remember all if/else statements across the codebase, and just add new one here:
const supportedTypes = {
News: el => !!el.label,
Bikes: el => !!el.storage && bikeCountsAreValid(el)
}
no if statements at all in our main function:
const isSuppotedType =
(el: IHeadlines) => supportedTypes[el.type] && supportedTypes[el.type](el)
Good clean code practices suggest that you have small functions and have a self-describing code.
In your code, I would make each Boolean logic become a variable or a function.
Example 1
func(element: IHeadlines): boolean => {
const isCarWithLabel = element.type === 'Cars' && element.label;
if(isCarWithLabel){
return true;
}
const isBikeWithStorage = element.type === 'Bikes' && element.storage;
if(isBikeWithStorage){
// rest of your logic that I don't know...
}
return false;
}
Example 2
func(element: IHeadlines): boolean => {
return this.isCarWithLabel() || this.isBikeWithStorageAndSomethingElse();
}
I'll suggest you something like this:
func(el: IHeadlines): boolean {
return el.type === 'Cars' && !!el.label ||
el.type === 'Bikes' && (
!!el.storage?.find(el => el.id == 1) || el.storage?.filter(el => el.id == 2).length > 1
);
}
Here is my method:
Object.entries(query).forEach(([key, value]) => {
if (key === 'team_ids') {
if (typeof value === 'string') {
this.items.push(this.$store.getters.teamById(value));
} else {
value.forEach((itemId) => {
this.items.push(this.$store.getters.teamById(itemId));
});
}
else if (key === 'close_ids') {
if (typeof value === 'string') {
this.items.push(this.$store.getters.closeFriendsById(value));
} else {
value.forEach((friendId) => {
this.items.push(this.$store.getters.closeFriendsById(friendId));
});
}
} else {
if (key === 'name') this.name = value;
if (key === 'patr') this.patr= value;
}
});
I am trying to refactor it but now i'm stumped...
It don't looks good.
Any advice?
You can refactor if statements with a switch statement.
Try this:
Object.entries(query).forEach(([key, value]) => {
switch(key) {
case 'name' :
this.name = value; break;
case 'patr' :
this.patr = value; break;
default:
let getterMap = {
'team_ids': 'teamById',
'close_ids': 'closeFriendsById'
}
if(Array.isArray(value)) {
value.forEach((itemId) => {
this.items.push(this.$store.getters[getterMap[key]](itemId));
});
} else {
this.items.push(this.$store.getters[getterMap[key]](value));
}
break;
}
});
You can add more keys in getterMap if you want to.
It's not bad, You have ternary operators which make code cleaner and if statements are shortened. However, to if you want to refactor it, you should provide some info about the logic, because it is important in refactoring.
I have a data set of exercises that can be done when working out, in JSON format. I'm implementing a filtering system on this data, which has three different types of filters users can apply: sort filter, (which takes the current set of exercises and orders them by name a-z or vice versa), muscle group (exercises that fall under the same targeted muscle group) and training program (exercises that fall under the same training program).
Users should be able to select any of the filters and it will apply it straight away, but not all 3 types of filters have to be applied. Therefore, I have come up with the following function in JS:
function applyFilters(filters) {
const sort = filters[0];
const muscles = filters[1];
const programs = filters[2];
const exerciseJSONData = "exercises.json";
$.getJSON(exerciseJSONData, {
format: JSON,
}).done(function (result) {
let filteredArr = [];
if (muscles.length === 0 && programs.length === 0) {
filteredArr = result;
} else if (muscles.length !== 0 && programs.length === 0) {
$.each($(result), function (key, val) {
if (muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0)) {
filteredArr.push(this);
}
});
} else if (muscles.length === 0 && programs.length !== 0) {
$.each($(result), function (key, val) {
if (programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)) {
filteredArr.push(this);
}
});
} else {
$.each($(result), function (key, val) {
if (
muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0) &&
programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)
) {
filteredArr.push(this);
}
});
}
$("#number-of-exercises").text(filteredArr.length + " Exercises Found");
$("#exercises").empty();
createExerciseHTML(filteredArr);
});
}
This function works as intended, however, I think the readability of the if else statement could be hard to understand for others and breaks the rule of not repeating code. I am wondering if there is a more concise and efficient way of coding this function? Thanks in advance.
P.S. Sorry if there was too much info at the beginning of this post, I wanted to give some context to avoid any confusion readers may have.
You can start by taking the $each out and keep the if in the each.
function applyFilters(filters) {
const sort = filters[0];
const muscles = filters[1];
const programs = filters[2];
const exerciseJSONData = "exercises.json";
$.getJSON(exerciseJSONData, {
format: JSON,
}).done(function (result) {
let filteredArr = [];
var hasMuschle = muscles.length > 0 ? 1 : 0;
var hasProgram = programs.length > 0 ? 1 : 0;
var muscleAndProgram = hasMuschle - hasProgram;
$.each($(result), function (key, val) {
if (muscles.length === 0 && programs.length === 0) {
filteredArr = result;
} else if (muscles.length !== 0 && programs.length === 0) {
if (muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0)) {
filteredArr.push(this);
}
} else if (muscles.length === 0 && programs.length !== 0) {
if (programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)) {
filteredArr.push(this);
}
} else {
if (
muscles.some((item) => val.MainMuscleGroup.indexOf(item) >= 0) &&
programs.some((item) => val.TrainingProgram.indexOf(item) >= 0)
) {
filteredArr.push(this);
}
}
})
$("#number-of-exercises").text(filteredArr.length + " Exercises Found");
$("#exercises").empty();
createExerciseHTML(filteredArr);
});
}