DEV Community

Discussion on: 150+ Typescript one-liners [code snippets].

Collapse
 
joelbonetr profile image
JoelBonetR 🥇 • Edited

Hi Luke,
I'll try to answer your questions or points one by one below

I prefer his isEmpty because I will not have to wrap it in a try/catch like yours

it is not a try catch, it is a throw, which is meant to articulate user-defined exceptions.

It is used as "offensive programming" tool and it should be used whenever defensive programming doesn't fit in the specific use-case.

Read more about defensive and offensive software design.
A bit excerpt from that:

Generally speaking, it is preferable to throw exception messages that enforce part of your API contract and guide the developer instead of returning error code values that do not point to where the exception occurred or what the program stack looked liked, Better logging and exception handling will increase robustness and security of your software, while minimizing developer stress.

As extra info, a try...catch statement will only reach the catch if any Exception occurred inside the try execution block. It can be either a language pre-defined exception or a user-defined one (using throw).

You should use try...catch when calling functions that throw errors and maybe using the finally optional statement to ensure defensive programming if it suits.

Example:

  /**
   * Selects a row from User model by primary key
   * @param {number} userId 
   * @returns {Array<any>}
   */
  const getUserData = (userId) => {
    if (typeof userId !== number) throw `getUserData Error. Number expected but found ${typeof userId}`;

    return await Users.find({id: userId});
  };

  /**
   * Gets information about a given user by ID
   * @param {number} userId
   * @returns {Array<any>}
   */
  const getUserInformation = (userId) => {
    let info = [];
    try {
      info = getUserData(userId);
    } catch (err) {
      console.error(`getUserInformation Error. ${err.message} in ${err.stack}`);
    } finally {
      return info;
    }
  };
Enter fullscreen mode Exit fullscreen mode

You'll notice that getUserData handles it's own exceptions (as it should be) and that, the caller ( getUserInformation in this case) has a try...catch.
If you send a string into it, it will try to call getUserData with this string and getUserData will throw an exception to the caller, that will be captured by the catch statement.

In this case we apply both offensive and defensive programming designs. Notice that getUserInformation will always return an Array, either be empty or filled in with the data of that given user from the DB.

In this case it may be useful to use

const userControllerExample = (user) => {
  if (isEmptyArray( getUserInfo( user.id ) ) ) throw `userControllerExample Error. No data was found searching for user ${user.id}`;
}
Enter fullscreen mode Exit fullscreen mode

and so on.

Hope now you find it as useful as it is IRL.

The idea of using TypeScript is that folks will get an error in dev time when trying to pass something to isEmpty that isn't an array, so they'll not be able to do isEmpty() or isEmpty(null) because they'll get compilation errors.
Even if the people using your lib is not using TS, they still get the type checking benefits (red underline when they pass something that isn't valid).

You may need several manual hard work to reach that, specially when dealing with different microservices.

If you receive a string or a undefined on a property after an API Call (in runtime, of course) when you expect an Array, either be by any mistake on the other side or by lack of data, and you try to pass isEmpty( myResponse.info ) you may get one of those:

isEmpty(undefined) // false
isEmpty('') // false
isEmpty(null) // false
Enter fullscreen mode Exit fullscreen mode

And the purpose of the function isEmpty() is now missleading and useless, sending the issue to the next function in the stack:

if( !isEmpty( myResponse.info ) getPreferences( myResponse.info );
Enter fullscreen mode Exit fullscreen mode

So if myResponse.info is undefined, an empty string, null... you will get false, thus send this "allegedly non-empty value" to getPreferences, in which you'll get a weird runtime error like that Uncaught TypeError: Cannot read properties of undefined.

If you're objective here, you'll see that the isEmpty function is not exactly doing what anyone would assume it does.

The compiled code will be unnecessarily longer with that throw on it. Not to mention the error isn't localized as a type error could be on the IDE.

It's the responsibility of the engine to handle that and optimize it, not ours. As devs we need to ensure there are no errors in runtime as well and, in case any bug appear, we should provide the tools to find the origin of the issue as fast as possible.

Don't build software thinking on how the engine will interpret and handle it. There are good reasons for that:
1- There are more than a single JS engine (V8, Spidermonkey, webkit) and there are differences as well between V8 in Node than V8 in Deno to set some examples.
2- It may change at any time without notifying you. Any design flaw, optimization path or performance increase the maintainers of each engine find during their job will be prioritized, developed, merged and set into production and they may or may not set the details publicly.

If you still think is a better option to throw, then why are you even using TypeScript?

You should also use Throw along TS. Check the advanced types reference, specifically type guards.

I'm copying the example in TS doc for convenience:

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}
Enter fullscreen mode Exit fullscreen mode

It's just that in this specific use-case you'll be good either with TS or without it. You can also set JSDoc instead:

  /**
   * Checks whether an Array is empty or not
   * @param {Array<any>} arr
   * @returns {boolean}
   */
  const isEmptyArray = (arr) => {
    if (Array.isArray(arr)) return !arr.length;
    else throw `isEmpty error, Array expected but found ${typeof arr}`;
  };
Enter fullscreen mode Exit fullscreen mode

And the result will be the same adding compatibility with any TS project.

Don't get me wrong, I still don't think the isEmpty function is perfect, it could use a rename, and better generics:
export const isEmptyArray = ({ length }: ArrayLike) => length > 0;

This won't prevent it to evaluate undefined.length and throw Uncaught TypeError: Cannot destructure property 'length' of 'undefined' as it is undefined.

We got plenty of tools in programming language APIs and the major part of them implement try...catch and exceptions for good reasons (except from some "low level" languages such C in which there are no Exceptions, if you're curious about that I'm letting a paragraph below).

The first step is to learn the tools, then discern whether to use them and then use them. You'll become a better developer each time you learn something new during this process. 😁


In C, the errors are notified by the returned value of the function, the exit value of the process, signals to the process (Program Error Signals (GNU libc)) or the CPU hardware interruption (or other notification error form the CPU if any), for example, see How processor handles the case of division by zero. You still can handle some sort of exception-like manually using setjmp.h.