Don't use functions as callbacks unless they're designed for it
jakearchibald.com
jakearchibald.com
In most other languages where unwanted extra arguments raise an error instead of being silently ignored, this mostly isn't a problem
This is really just a wonderful example how javascript is a mental burden to the programmers instead of a useful tool.
TypeScript won't catch the original landmine because it ignores the extra parameters; maybe some linter would? Is there a rule that enforces "functions used by map must spell out all the parameters?"
* Example of typescript giving an error on toReadableNumber_v2: https://www.typescriptlang.org/play?#code/FAYw9gdgzgLgBAQwE5...
So username, password are distinct from string and objectid might be backed by an int, but is distinct from int.
Obviously, once compiled, there's no overhead, it's a zero cost abstraction. But an incredibly useful one.
Scenario: Library changes signature so you always pass functions with more parameters.
Result: This will break almost every codebase, they probably wouldn't do this in a minor patch.
Scenario: Library adds a new signature where you can pass functions with more parameters and keep them overloaded between each other.
Result: Compiler can't identify which of the two signatures to pass your overloaded function and throws a compilation error.
A type check could prevent this because it would require map to take a reference to a function with three parameters, or the compiler would complain inside the map implementation.
Similarly, passing it a function with only one parameter would be a type violation and the compiler would complain.
Now in a language with type checking, you could still potentially run afoul.
Say the map function was overloaded with one variant for one-parameter callbacks, one variant for two-parameter callbacks etc. Then the compiler might figure out it could use the second overload if the "toReadableNumber" function got changed to take the extra "base" parameter.
So again you end up with the numbers getting converted with a variable base.
Though, IMHO, having such an overloaded map function is inviting trouble and is a very poor design.
[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Functions can be assigned at runtime. It would have to be a runtime error.
So it's NOT a runtime error, it's a compile-time error, even though you can assign different functions at runtime.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Static code will get checked at compile-time, even though the exact function passed as callback depends on the incoming data via a series of if/case statements say, from a dictionary or whatever.
And it only works because `Array.prototype.map` callback has one required arguments and two optional ones. What language with optional function arguments protects you from this sort of behavior? Do people in that language not test this sort of behavior? Or at least run through the app?
More than that, the whole hurrdurr-javascript-bad thing is so tired. Lots of work is being done in JavaScript. Sure, it has its quirks, but those quirks come with expressiveness. You can use functional or imperative style, throw lambdas around, and it runs on pretty much all the phones and computers in the world.
Languages with a sane type system.
TypeScript has no issues catching such mistakes. Writing tests to catch simple type errors is such an incredible waste of time.
TypeScript doesn't complain when you pass in a function that ignores some of its arguments. Which is totally fine and safe. If you upgrade your function from no second argument to a numeric second argument TypeScript will not complain and your program might break.
It will not crash, because it still perfectly type-safe, but it might not behave like you want to. So in that sense the article has a point.
However, this is just one instance of a larger issue with changing the behavior of a function, while keeping the types compatible.
- Using a function as a callback to .map() with two numeric arguments and then swapping the arguments.
- Returning a tuple of two of the same type and swapping the order.
- Returning a string in a new encoding.
- ...
Basic rule: if it's a type error and the program might crash, TypeScript will complain. If the types are fine and only the behavior changes, TypeScript will (obviously) not complain. Callback functions and optional arguments are not special in this regard.Which is generally worse than crashing, because silent data corruption can have far-reaching impacts.
> If you upgrade your function from no second argument to a numeric second argument [...] It will not crash, because it still perfectly type-safe
It's type-safe using a definition of "type-safe" that is defined relative to the underlying JavaScript model of "corrupt the user's data rather than crash". It wouldn't be type-safe using most other languages' (including Python, for example) model of "functions have a type that includes the number of arguments, and applying them to the wrong number of arguments is meaningless and hence not considered type-safe".
It's fair for TypeScript to use this approach. But it is surprising to many of us, who view type systems as tools for ruling out some dumb functional bugs, not just crashes.
Also, > A lot of work is being done in JavaScript
Is that it? It's one of the best funded languages on Earth, what do you expect - with D for example we can do all the things you mentioned and catch errors like this, and we're basically just some guys working on the language not the combined might of the entire Internet sector.
Array.prototype.map = function (func) {
if (func.length !== 3) {
throw new Error(
"bad! this function you've passed must take THREE arguments. Grr!"
);
}
for (let i = 0; i < this.length; i++) {
this[i] = func(this[i], i, this);
}
return this;
};
;)(making this workable and bug free is left to the reader or multi-billion dollar corporations)
There can be a non-indexed map as well.
I'm most curious as to who uses the last argument in the JS map function!
array.map((val, index, {length}) => if (index + 1 == length) { alert("last element") } else { alert("element #"+index) })
It's convenient for some things that would otherwise require a reduce (but where reduce isn't particularly more efficient, because you just need lookahead/lookbehind) or an imperative loop, like transforming a list to a set of moving averages over the list.
It's a little more expressive than reduce our imperative lots loops in those cases, too.
The true nature of type checking is basically a method of hindering you. The set of all correct programs is much smaller then the set of all programs that exist so anything that hinders a programmer from operating in the bigger parent set outside the set of correct programs is a good and impressive thing.
What’s going on here is a type checking issue. JavaScript and typescript is a little too loose. The map method takes a function of <Arity 3, 2, or 1> So if a library changes a function from <arity 1> to <arity 2 or 1> you should get a type error, but the type checker is too loose. It’s subtle.
Basically a type of <arity 3, 2, or 1> should only type check with <arity 3> or <arity 2> or <arity 1> it should not allow <arity 2 or 1>. You see what’s going on here? Subtle.
This is indeed as much of a type checker problem as it is defining what type correctness is. The definition above is simply a way of defining type correctness that fits with our intuition of what is correct for this given situation, so take what I wrote with a grain of salt. There could be situations where the current definition of type correctness in typescript is more correct then the definition I provided.
Our intuition is complex and if you think long and hard enough you may be able to come up with a formal definition of type correctness that perfectly fits our intuition and therefore elegantly unionized typescripts looser definition of correctness and my own stricter definition.
Beware though, often human intuition can be contradictory. This means that a formalization of our intuitive notions of type correctness will also be contradictory and therefore unusable. In other words there may not be a way to type check for this issue while maintaining the convenience of the status quo.
Intuitively I think it’s possible, you just need special syntax to tell the type checker whether to use my stricter definition or the original looser definition that’s in use now.
Also I’m not sure if there’s any type checker in existence that handles that case (don’t know). So I believe this is more than just a JavaScript issue.
It is my very strong suspicion that in almost all (if not all) of the similar cases in programming methodology, there have been not just arguments on both sides, but implementations and real-world lessons on both sides.
A veeeery minor example: I’m equally as sure that someone designed and deployed systems that specifically created errors when you tried to pass less than the required number of params as I am that other people (or even the same people) specifically designed and implemented systems where params were optional (most likely because they hated having to specify empty params every time).
Sometimes a dynamically typed language is the right tool for a job, but IMHO this mostly holds for auxiliary stuff. Typed languages are more effort during programming/learning, but the benefit is gigantic.
But maybe I'm just too biased because I'm usually involved with "is this fails, people may [literally] die".
const func = (i: number, x: boolean) => i * 2
const nope = [1, 2, 3].map(func) // type error!Unfortunately, this does mean TypeScript’s type system can’t be entirely sound. A classic situation that is also legal according to the rules of TS but “ought” to fail type checking is something like this:
let arr_num: Array<number> = [1, 2, 3]
let arr_opt: Array<number | null> = arr_num // Erm...
arr_opt[0] = null // ERM!!!
Now arr_num[0] is null, clearly violating the intended type constraint.This problem could be fixed by making it an error to alias arr_opt to arr_num. However, that might also cause a lot of extra work for anyone trying to migrate an existing JS code base, particularly if the types involved are not of their choosing but instead determined by code written elsewhere.
For example, if you called a library function that returned an Array<number> and you passed that into another library function that required an Array<number | null> and wasn’t going to modify that array, enforcing the constraint could mean that working code was broken for no real benefit.
Then you get into deeper questions about enforcing immutability using the type system, and finding that again you’re building on sand because you still have JS underneath. IMHO, it’s hard to blame the TS designers for not wanting to go down these kinds of rabbit holes.
However, you don’t see the same warning in the case of functions that can be called with variable numbers of arguments if the types of the arguments being unintentionally supplied do match, because within the rules of TS, this is working as designed.
Combined with the perhaps unfortunate decision to provide a standard `map` function that doesn’t use its callback as most languages do, there is still the potential for an unexpected change of behaviour that the type checker can’t warn you about here.
Yes, TS is fine with passing more arguments to a callback that takes fewer. The callback cannot possibly use the additional arguments, so it doesn't matter what gets passed as it will not change the outcome.
This is very different from passing the wrong kinds of arguments to functions that do read them and do something with them, like parseInt.
Now, if you decide to pass a function with an optional second argument that matches the second argument that will get passed to the callback and expect that it will not be used because why would anyone pass additional arguments to a map callback - then yes, you will have the problem again.
function addOneByDefault(num: number, addAmount = 1) {
return num + addAmount
}
[1,2,3,4].map(addOneByDefault) // this typechecks but works poorly
This extra example is missing in the article and might be helpful to add.The type checking I am talking about is not a sum type. It is not that the function can take a two different possible types. It's the fact that the parameter function can mutate into two different types depending on the usage. It has (<arity 1 or 2>) not (<arity 1> or <arity 2>) if you catch my meaning.... Or in other words the concrete type is not evaluated when you pass the function as a parameter but only when it is called with a certain amount of parameters... which is not something type checkers I know about look for.
Perhaps I’m not correctly understanding your idea around arity as part of the function types, but so far it’s not obvious to me how what I think you’re describing helps to resolve that contradiction. Are you suggesting a way the type system could be changed without causing those additional, unwanted side effects?
Do you by any chance have a more rigorous definition or even a formal semantics for your proposed arity types that you could share, so the rest of us can understand exactly what you’re proposing here?
You don't need to change the behavior of the program. You can change the type checker to catch the unwanted error.
>Perhaps I’m not correctly understanding your idea around arity as part of the function types, but so far it’s not obvious to me how what I think you’re describing helps to resolve that contradiction. Are you suggesting a way the type system could be changed without causing those additional, unwanted side effects?
It's not formalized anywhere to my knowledge and I'm not willing to go through the rigor to do this in the comments. But it can easily be explained.
Simply put, what is the type signature of a function that can accept either two variables or one variable? I've never seen this specified in any formal language.
To fix this specific issue you want the type signature here to specify only certain functions with a fixed arity.
When some external library is updated with a function that previously had arity 1 to <arity 1 or 2> that could be thought of as type change that should trigger a type error.
Right now type checker recognizes F(a) and F(a, b=c) (where c is a default parameter that can be optionally overridden) as functions with matching types.
F(a) == F(a, b=c)
F(a,b) == F(a, b=c) <-----(F(a,b) in this case is a function where b is NOT optional)
F(a) != F(a, b)
From the example above you can see the type checker lacks transitivity (a == c and b == c does not imply a == b), because the type of a function with an optional parameter is not really well defined or thought out.This is exactly the problem the author is describing. The type checker assumes that when the library changed F(a) to F(a, b=c) that the types are still equivalent, but this breaks transitivity so it's a bad choice and will lead to strange errors because programmers assume transitivity is a given.
You don't see this problem in other type checkers because JavaScript is weird in the sense that you can call a function of arity 1 with 5 parameters.
function toReadableNumber(num, base, trap) {
if(base == undefined) base = 10;
if(typeof base != "number") throw new Error("Second argument should be the base! base=" + base + " (" + (typeof base) + ")");
if(trap != undefined) throw new Error("Did not expect a third argument. Are you using this with map? Then use an intermediate function.");This is a problem that should be handled at the language level, not by adding multiple lines of potentially incorrect code for every dozen lines of regular code.
Also you should wait until your API is somewhat stable before adding the guards. So for most code, you do not need guards. But if your code is used by many, that defensive coding/guards, taking only a few minutes to add, will save countless man-hours that would otherwise be spent debugging.
As a general rule I like errors to throw early. So when I found a bug, (I first write a test to automatically reproduce the bug, then) I backtrack and add guards to each step (with helpful debug/data in the error message), so that the bug would be caught at the surface, rather then causing weird issues several layers down.
And guards are much easier to write then complicated type definitions. And the errors will be more informative, helpful and human friendly then errors from a type-checker.
Defensive coding is mostly useful in long living apps that have a lot of state, and which is constantly developed (new features added, breaking changes, etc). You would not need defensive code in programs that are executed once and then thrown away.
Most definitely not. It's not allowed in Python, it's not allowed in Ruby, it's not allowed in any Lisp I know of[0], … it is allowed in PHP, which is about what I'd expect from that[1]. In most dynamic languages the arity is not a suggestion[2].
Which is exactly the issue at hand: `Array#map` was (stupidly) defined as calling its callback with 3 parameters. The last 2 are useless 99.99% of the time (and in better language you'd compose them in if and only if you needed them), as a result it's almost universal that you'd pass single-parameter callbacks which works… until it doesn't because the callback now takes 2+ parameters and starts taking in account the previously ignored garbage `Array#map` feeds it. The average JS developer likely doesn't even know Array#map callbacks receive 3 parameters, and usually aren't going to think about it: in 99% of cases it's has no relevance whatsoever.
[0] but most lisps make significant uses of variable-arity functions, which is a very different and much more formal proposition
[1] PHP's one saving grace being that HoFs have historically not been much of a thing, though I have not tracked how it's used these days
[2] as long as it's present at all AFAIK in Perl functions don't have formal parameters lists
def hello(a, b = 'world'):
print(a, b)
hello('hello')
hello('hello', 'world')
But these don't, so fair point: hello('hello', 'world', 'there)
# nor
def hello(a):
...
hello('hello', 'world')In python those functions would look like:
def F(a = None, b = None, *args): ...
And you can't write any other kind of function in javascript. I don't really like that aspect of javascript, it creates so many hard to debug situations.While blocks, procs, and lambdas all have arity metadata, only lambdas check for the argument count when called. The other two drop excess arguments and fill missing arguments with nil.
If you're inlining your block as a literal do-end block on the call site, it's just a matter of knowing what kind of data you're calling the block-taking method on. So blocks are kinda different.
If you're designing a more intricate piece of code to be used repeatedly by a 'map' or 'reduce' (like in the example), nothing is preventing you from defining a lambda instead of proc. And nothing is preventing you from designing your library so that it exposes only arity-checking lambdas to the outside.
But it's also quite usual to define callbacks as plain old methods (e.g. Rails before and after actions). Methods can be easily used as a block by getting the actual Method object first with the 'method' method, then using the & syntax to automatically convert them to a proc (e.g. map(&method(:foobar)) which again, converts them to arity-checking lambdas.
As you mentioned, lambdas and methods check for that, but it’s sad to have to give up the syntactic and lexical niceties of blocks.
Is this a "in the javascript world everything is a callback" thing, or is the author just using the term loosely?
Maybe you should check your bias about the JS world (as well as your definitions).
This is not helping beginners and it make documentation and qa especially hard to search.
This article uses the term callback but then uses functions passed to "map" as its examples; "map" is synchronous and so this usage of the term "callback" is atypical, probably atypical to the point that we can just say it's incorrect :).
0 - https://en.wikipedia.org/wiki/Callback_(computer_programming...
I’m not aware of any authoritative definition, but the term “callback” has been used for functions passed in as parameters to higher-order functions since long before the modern idioms for asynchronous code were around. The first use of the term I can remember personally was for the comparator function passed to qsort in C. That was probably sometime in the 1980s. Another common usage that goes way back is for event handlers in event-driven systems.
The term is actually used correctly; callback's definition is broad. Of course, noone can resist the temptation to berate members of the JS community.
[array mapSelector:@selector(toReadableNumber:withOptions:)
fromObject:[NSFormattingManager localizedFormattingManager]
withArguments:@[@{kCFNumberFormattingBaseKey:@(10)}]];
Thank god they invented @-syntax for core type literals.A better language might have a special syntax or specific types for mapping functions but that's not the argument you were making.
- Version should be bumped - Dependencies should be informed via the changelog - Existing tests of dependencies should fail
Not if your language has default parameters and does not allow callers to pass "extra" parameters. Then you can easily add a new parameter with a default value and older callers will work as expected.
TBF adding parameters is a breaking change in most languages unless they have defaults, or even then.
In javascript it's a corrupting change, it may silently break all callers.
No I don't? A breaking change means working code doesn't work anymore. In a statically typed language, if a dependency adds a parameter to a function your code stops compiling. That's very much a breaking change.
Original function:
doSomething(foo){
*body*
}
Refactored functions: doNewSomething(foo, bar){
*body*
}
//now a convenience function doSomething(foo){
bar = defaultValue;
doNewSomething(foo, bar);
}
Doesn’t this solve this when updating a library?Like if you only pass one variable, you get the old behavior, and to pass two variables you have to call a new function name.
What am I misunderstanding about what you are writing.
toReadableNumberWithBase(num, base)
Optionals arguments (ideally named and with default values) are quite easily dealt with and the developer is aware he'll break compatibility if he touch them. No error on extra argument is another flavor of madness.
eslint-plugin-unicorn has a lot of great rules, some are opinionated but you don't have to use all the rules.
In this specific case the function was already callable with two parameters, without the library author's intent.
Another example to the general rule is the fragile base problem: every method is a customization point by default.
These "convenience" features can easily turn into headaches like this for library authors.
Literally any kind of change to a function in javascript is a breaking change.
Don't believe it? Give me an example of a function and how you change it and I will show you the code that works with the original function but breaks with the changed one.
add({a:2, b:4})
function add({a, b}) { return a + b }
function _add({a, b, multiplier = 1}) { return a+b*multiplier }
challenge: come up with a breaking use that does not involve something having a property named "multiplier"
However, let's keep the tension for while. The fact that having a working function using "multiplier" is already a bit broken - it shouldn't work from the beginning, but javascript is designed so that it does. Hence you have to give me this restriction, because otherwise it is obvious how your example can be "broken".
Before I expose my (very simple) solution to still break your example even without using "multiplier", I would like to ask you to try to come up with another example first, where you don't require restrictions. I think it's a good exercise. :)
If no one comes up with one, I'll show it in, say, a day from now.
console.log(add({a:"1", b:2})) //12
console.log(_add({a:"1", b:2})) //21
So to make the code break with the change to _add, I can just do: if(add({a:"1", b:2}) != 12) boom()For day to day, I feel this pattern is good enough, as typescript works nicely with it.
A tricky thing I could see without ever explicitly defining "multiplier" (e.g. on the object prototype) is passing a Proxy that e.g. has a fallback for all missing properties. Detecting a proxy is only kind of possible (?) but we can copy all the original target properties from it, which should make it safe.
So here goes, my safe solution for modifying function signature in a non breaking way:
function add({ ...args }) {
const { a, b, ...rest } = args;
if (typeof a !== "number" || typeof b !== "number") {
throw "all arguments must be numbers";
}
if (Object.keys(rest).length > 0) {
throw "You may only pass arguments a and b";
}
return a + b;
}
function _add({ ...args }) {
const { a, b, multiplier = 1, ...rest } = args;
if (
typeof a !== "number" ||
typeof b !== "number" ||
typeof multiplier !== "number"
) {
throw "all arguments must be numbers";
}
if (Object.keys(rest).length > 0) {
throw "You may only pass arguments a, b and multiplier";
}
return a + b * multiplier;
}
Most of these issues (not the proxy one) should be solved by typescript.In the evil world, I can break your code like that:
try {
add(1, 2, 3, 4)
} catch (e) {
if(e !== "You may only pass arguments a and b")
throw "boom";
}
However, you can of course make your exception string generic.Then I'll have no choice to use one of my jokers: calling "add.toString()" and inspect your function in detail. Before you scream that this is stupid, please mind that this is actually used out there (looking for example at you, angular).
function add({ ...args }) {
const { a, b, ...rest } = args;
if (typeof a !== "number" || typeof b !== "number") {
throw "no";
}
if (Object.keys(rest).length > 0) {
throw "no";
}
return a + b;
}
function _add({ ...args }) {
const { a, b, multiplier = 1, ...rest } = args;
if (
typeof a !== "number" ||
typeof b !== "number" ||
typeof multiplier !== "number"
) {
throw "no";
}
if (Object.keys(rest).length > 0) {
throw "no";
}
return a + b * multiplier;
}
add.toString = () => "nice try";
_add.toString = () => "nice try";
Edit: OK I think we are stretching HN comment ettiquete to far with this much code. This was fun though. Thanks. if( Function.prototype.toString.call(add).includes("multiplier") ) throw "boom!";
> Edit: OK I think we are stretching HN comment ettiquete to far with this much code. This was fun though. Thanks.Huh? Would you mind to educate me about what part of the ettiquete we are not following?
At this point we can go ahead and break the world:
add.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;
_add.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;
Function.prototype.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;Now, we are leaving the original scope (not just changing a function, but modifying globals). read-only globals even. But prepare for my counter:
let frame = document.createElement('x');
document.body.appendChild(frame);
if( frame.contentWindow.Function.toString.call(add).includes("multiplier") ) throw "boom!";
You might go to also kill "document.createElement", but there are many ways for me to get a new frame. I think when we come to the point where all these are disabled, I would say only a small fraction of the websites that use javascript would still properly operate. It would be your victory though. ;)That is stupid though. It's like saying changing a private field in Java is a breaking change because someone might have used reflection to access it.
Taken to the moronic extreme: any detectable change is a breaking change because someone could write a function that pulls your latest release and depends on every bit being identical with the previous release.
> It's like saying changing a private field in Java is a breaking change because someone might have used reflection to access it.
Which is true, both in theory and practice.
Even look at misc.Unsafe - which is deliberately named unsafe and everyone was told not to use it. Then they tried to drop support for it and people freaked out so much that support was continued. (https://jaxenter.com/java-9-without-sun-misc-unsafe-119026.h...)
> Taken to the moronic extreme: any detectable change is a breaking change because someone could write a function that pulls your latest release and depends on every bit being identical with the previous release.
I would say it is best described here: https://xkcd.com/1172/
f = Object.defineProperties(x => x, {toString:{value:()=>'a'}, [Symbol.toStringTag]:{value:'a'}})
and we will change it to: f = Object.defineProperties(y => y, {toString:{value:()=>'a'}, [Symbol.toStringTag]:{value:'a'}})
Edit: ah you -can- break it actually. A challenge for others to figure out how to break this one.logging_id = x => { log(x); return x };
https://news.ycombinator.com/item?id=26039826
Strange coincidence.
Edit: I always wondered if jquery's .each deliberately had a signature of function(i, ele) to discourage people from mistakes like this, or if it was a happy accident.
Though on second thought, maybe not a great question, hard to say. I know I've tripped up on the std sort function having not had used it in a while.
If you have to have optional arguments, its probably best to make them named.
This would this make an unintended clash a lot less likely. The name has to match - and if you have the types, both the type AND the name would need to match.
Not only that, it also it lets you have any number of optional arguments with a lot less fuss, and pass any subset of them.
The inmates are not only running the asylum, they built it too.
A strongly typed compiler would not catch the error in the article with a similarly overloaded map function, so type checking can't really rescue you from this situation.
Sure the chances of it happening silently might be slightly less, as the second and third parameter would have to match in type (integers in this case), but it could still absolutely happen silently.
So to me the core take-away is that overloading a function in such a way is a very poor design choice, regardless of language.
> Ignores the second parameter if it's not an object so you can work with arrays better like .map(read)
It's also pretty simple to implement:
// Assuming we are taking an options object {}
export const myFunc(arg1, arg2 = {}) => {
if (typeof arg2 === 'number') arg2 = {};
// ... rest of the code as usual
};
So, library authors, do a favour to users and in those functions that could be used as a callback, add this option.Some other tips/niceties I've learned over the years:
- For highly async libraries, allow to accept an unresolved promise. It's pretty safe and easy to do on the library-side, and will probably remove a bug or two on your users' side.
- Export default and named so the users don't need to worry about whether to `import * as files from` or `import files from` (assuming the library is small and there's no concern about tree-shaking).
- I also use a higher-order promise abstraction I created, Swear (https://documentation.page/github/franciscop/swear), but that's totally optional and you can treat any of my async libraries as normal Promises.
But I definitely can image typescript could provide another "strict" (or even something outside of "strict" group, like recent "pedantic" option for index access [1]) option that would check against that potential errors.
I see people using .filter(Boolean) a lot for example. If the signature of that were to change, for sure it wouldn't be something that quietly gets implemented, but I wouldn't pass formatting functions to these operations carelessly, especially in a codebase where there may not be tests. Some of the safer ways I've seen are use of unary helper functions to wrap the callback, or having the callback actually take the arguments but discarding them like (element, _index, _array). At least that way you communicate some intent.
The habit that I developed as a response to that is to always pass a lambda to the callback. That is, write this:
foo(x => f(x))
instead of foo(f)I'm used to statically typed languages that allow calling HOF's with a function reference. If the signature matches, there's no reason to wrap it in a lambda.
So I did the same, reflexively in a Node project recently.
I've learned my lesson and now I just know to always use a lambda forJS function arguments.
A type system may help, but the fact persists. Type systems only catch changes in (args.length ++ args.map(primitive_typeof)). I think that this issue is not with types or arguments, but with a loose naming and handling of dependencies and backwards compatibility. A theoretical author of toReadableNumber() simply ditched one function and introduced another one, in-place. Apparently they did that because there is no way in their project to fork, retain and maintain both. You may say that types solve 95% of this, but dynamic languages exist for a reason, and when you feel like using one or see an advantage in it, other techniques may be applied. We could resolve that by using semver on functions instead of modules (renaming them at import for convenience), but nobody does that. Functions are fundamental building blocks, they take your args, do their job, return results and may have a separate environment (not in js), but somehow they are not autonomous entities. In contrast, in a real world we use explicit versions of things: gtx 1060 6gb, iphone se, cat 6020b, and the same for their part numbers. Nobody specifies just “gtx” or “cat” in their package.xls.
Don't attempt to code for every possible future.
This type of behavior is exactly the reason why I think the existence of Typescript adds downsides to being a JS dev.
Saying "just use TS" is of equal value as saying: "just use Assembly" or "just use Dart". It has no value.
First an foremost this type of logical behavior is a problem that needs to be adressed in JavaScript. The dynamically typped language that is embedded into every major browser. A heuristic will have to be discovered by JS devs. Or TC39 will have to extend the standard.
Ultimately, it then comes down to a contexual personal choice of using TS over JS.
When it comes to separation of concerns, I can recommend reading this essay by Dijkstra: https://www.cs.utexas.edu/users/EWD/transcriptions/EWD04xx/E...
JavaScript is a lost cause to me. I'm not a full time JS Dev, so every time I have to touch one of our Node projects at work, I mentally prepare myself for very slow dev speed (ironic, considering the arguments that dynamically typed, loosey goosey, language speed you up) and frustrating bugs around `this`, mixed up function arguments, forgetting to await promises returned from functions, etc.
But TypeScript really doesn't actually help that much compared to an IDE that understands JSDoc. Its type system is unsound and it's too accommodating of JavaScript's nonsense.
If I ever start a new project that just has to run on Node, I'd probably try one of these languages that transpiles to JS, but is totally different, like Clojure or OCaml.
I enjoy TS and for web applications I wouldn't want to do without it, but it for sure isn't the "end all" since it still needs to work with those issues. Ultimately though in the case of some of the examples, a type system in a language with optional parameters doesn't excuse you from having to use your brain. Especially if you're the kind of developer who thinks testing your code is just unnecessary.
I'm not a frontend guy, but I truly don't know what I'd do with a frontend project. ClojureScript? Elm? OCaml? I would even do JavaScript with JSDoc comments before I'd bother doing TypeScript.
From a quick web search, it looks like JavaScript interop isn't totally frictionless, which is probably a good thing to- as crazy as that may sound.
But it looks like it does rely on making up a type signature for the JS you call into. I assume there must be a way to use TypeScript signature files, too.
It's interesting. I'd like to look into it some time.
As one fun example suppose you're working with the snippet:
const f = foo.predicate; return arr.filter(f);
If foo is a class instance and references any instance variables via `this`, then just storing the method before you attempt to use it will cause the whole house of cards to blow up. So...adding a reference to `this` is a breaking change for even moderately sane code. That problem is also easily mitigated with lambdas: const foo = (x) => foo.predicate(x);
Thanks for writing this. It's bookmarked and will share it with my team when necessary.
That does sound very unlikely.
Practically speaking, I've virtually never run into this problem in 6 years of writing JS professionally. Of course your mileage may vary
It should be "Don't use functions as _arguments_ unless they're desdigned for it".
Function passed as arguments to higher-order function are just that, arguments.
A function callback on the other hand is just that, a call back, after a longer asynchronous run.
To me that’s the real point. In JavaScript where a function’s signature is not changed by the number of parameters, adding even an optional argument probably constitutes a breaking change.
Who cares about the web, right?
While people keep crying about how bad JS is, I (and many others) use it for what it is, a tool with his flaws but also its advantages. I wrote Rust, Python, PHP, a bit of C and honestly JS is still my favourite language to write together with rust.
People are lame.
const readableNumbers = someNumbers.map(item => toReadableNumber(item));