DEV Community

Cover image for Nested Conditional Operators
Avalander
Avalander

Posted on

Nested Conditional Operators

Cover image by Talles Alves on Unsplash

Internet wisdom says that nested conditional operators are evil. And I firmly believed internet wisdom until today.

Today I found myself refactoring some old Javascript code and it had one of those functions that validate that a heckload of properties exist and have the right format in an object. Then I went crazy and wrote something similar to this.

const validateInput = ({ field1, field2, field3 }) =>
    (!field1
        ? Promise.reject('Missing field1')
        : !Array.isArray(field2)
        ? Promise.reject('field2 is not an array')
        : !isValidType(field3)
        ? Promise.reject('field3 is invalid')
        : Promise.resolve()
    )

Don't mind the thing with promises, this function is used inside a promise chain.

The thing is, it makes me cringe a bit because, you know, nested conditional operators are evil, but I actually find it readable, and I'd say it might even flow better than the chain of ifs.

I would like to read your thoughts and opinions in the topic. Are nested conditional operators inherently evil and unreadable or are they alright, they have just been used in a messy manner for too long?

Oldest comments (48)

Collapse
 
emgodev profile image
Michael • Edited

I think the fact that the design of a conditional (especially ternary) is not restrictive says nothing of how it is used. I find in most cases where I desire to write less code, that using a normal if conditional is actually better even if it promotes greater nesting.

I have been developing more classic writing habits of late though, so take that with a grain of salt? I prefer conditionals with closure to support flexible scope without rewriting anything, and they clearly define line-breaks. My writing preference of your function would look something like this (I don't fully understand Promises yet):

function(f1, f2, f3){
  if( !f1 ){ new Error('Missing field1'); }
  if( !f2.constructor.name === 'Array' ){ new TypeError('field2 not an array'); }
  if( !isValidType(f3) ){ new Error('field3 is invalid'); }
}

Taking a look at your function, for example; I would write this as separate conditional checks because they are not related to one another. This is a pattern I find useful when designing other functions, often I only want to check for a single condition, not a chain of if/else conditions. In this function, you only want to return a rejection if a field fails a conditional check. Try to think of it as 'If this is true than do this', not 'if this is true do this, otherwise check for these things and do this'.

This is what the if/else would look like using normal conditionals.

if( !f1 ){
  new Error('Missing field1');
else{

  if( !f2.constructor.name === 'Array' ){
    new TypeError('field2 not an array');
  else{

    if( !isValidType(f3) ){
      new Error('field3 is invalid');
    }
  }
}

return true; // resolve

As for ternary conditionals, I only ever write them inline for simple checks and I have read and agree that they should not be nested more than 1-2 levels.

function test( a ){
  const valueExists = a ? true : false;

  if( valueExists){ return a; }
  // otherwise...(and no need for an else{})
  return false;
}
Collapse
 
joelnet profile image
JavaScript Joel • Edited

nested conditional operators are evil

I think nested anything is evil. So I prefer to call them Chained Conditional Operators. Kinda like promise chaining.

For a chained conditional operator, I prefer this style syntax, with the condition on the left and the action on the right.

const validateInput = ({ field1, field2, field3 }) =>
  !field1 ? Promise.reject('Missing field1')
  : !Array.isArray(field2) ? Promise.reject('field2 is not an array')
  : !isValidType(field3) ? Promise.reject('field3 is invalid')
  : Promise.resolve()

Then it looks similar to an if

const validateInput = ({ field1, field2, field3 }) => {
  if (!field1) return Promise.reject('Missing field1')
  if (!Array.isArray(field2)) return Promise.reject('field2 is not an array')
  if (!isValidType(field3)) return Promise.reject('field3 is invalid')
  return Promise.resolve()
}

or a switch

const validateInput = ({ field1, field2, field3 }) => {
  switch (true) {
    case !field1: return Promise.reject('Missing field1')
    case !Array.isArray(field2): return Promise.reject('field2 is not an array')
    case !isValidType(field3): return Promise.reject('field3 is invalid')
    default: return Promise.resolve()
  }
}

I prefer the chained conditional operator because it a function expression, compared to the others which are functions with block statements. I also really like the way it looks.

Cheers!

Collapse
 
avalander profile image
Avalander

Thanks for your input! I didn't know you could use switch that way! 😮

Collapse
 
joelnet profile image
JavaScript Joel

I didn't know you could use switch that way! 😮

You shouldn't, but you can 😉

Thread Thread
 
avalander profile image
Avalander

I don't think I've written a single switch statement since I left the Javaland, so nothing to worry about.

Thread Thread
 
link2twenty profile image
Andrew Bone • Edited

I wanted to suggest that way too but am on mobile 😁

Thread Thread
 
binarypatrick profile image
BinaryPatrick

Why shouldn't you?

Thread Thread
 
link2twenty profile image
Andrew Bone • Edited

And now I'm at a PC

I would take a similar route though I'd use false as the switch argument. Remember switch statements are if(x === y ) as opposed to if(x == y ) so we can't use truthy/falsey.

In order to make it work, I'd have a simple truthy to bool converter function. It takes up a bit of space but I think it's very readable.

const toBool = a => !!a;

const validateInput = ({field1,field2,field3}) => {
  switch (false) { // if anything fails
    case toBool(field1):
      return Promise.reject('Missing field1');
    case Array.isArray(field2):
      return Promise.reject('field2 is not an array');
    case toBool(isValidType(field3)): // this may not need toBool
      return Promise.reject('field3 is invalid');
    default: // If nothing fails resolve.
      return Promise.resolve();
  }
}
Thread Thread
 
skaterdad profile image
Mike

You could also replace your custom toBool function with the built-in Boolean constructor.

Thread Thread
 
link2twenty profile image
Andrew Bone

Oh wow, I honestly didn't know that existed. It looks like it does the exact same thing as !! so would be a great replacement.

Thread Thread
 
qm3ster profile image
Mihail Malo • Edited

@skaterdad @link2twenty the Boolean function not the Boolean constructor. The difference is important because

if (Boolean('')) throw "feces" // You'll be fine
if (new Boolean('')) throw "feces" // WATCH OUT, it's a truthy Boolean {false} object!!

And not even Boolean(new Boolean('')) will save you.
You'd have to go full (new Boolean('')).valueOf() to learn the truth.

(As opposed to String(new String(true)) and Number(new Number('2')) which do manage to extract the primitive value.)

Thread Thread
 
qm3ster profile image
Mihail Malo • Edited

I leave you with this:

const a = {toString: Math.random, valueOf: ()=>false}, {log} = console
log(String(a))
log(1 / a)
log('' + a)
log(Boolean(a))

Bet you won't guess the result of the last two without evaluating :v

let _ = {valueOf: Math.random}
_+=_
Thread Thread
 
qm3ster profile image
Mihail Malo

@joelnet Why shouldn't you use switch(true){case binaryexp: return stuff}? It's the same control flow as if(){} else if(){} else if(){} or if you are always returning if() return; if() return; if() return. What could go wrong? Fallthrough?

Thread Thread
 
joelnet profile image
JavaScript Joel

When given multiple ways to write something, I always prefer writing an expression over statements.

So I wouldn't use a switch or an if when I could use a ternary

Thread Thread
 
qm3ster profile image
Mihail Malo

I bet you're all about those throw expressions, aren't you? ;)

Thread Thread
 
joelnet profile image
JavaScript Joel

Hmm. Well, I have used function expressions to throw, so this would be an improvement.

But since we're on the subject of Exception, I'll use this opportunity to rant about Exceptions.

Unless your intent is to halt the program, an Exception should not be thrown.

If your function can result in an exception, return it. This would follow a similar pattern as Promises.

const failed = Promise.resolve('success')
  .then(data => {
    return Promise.reject('Uh oh!')
  })

failed
  .then(data => console.log('data', data))
  .catch(err => console.log('err', err))

And also similar to a callback:

const myAction = (input, callback) =>
  input !== 'good' ? callback(Error('uh oh')) : callback(null, 'YES!')

myAction('bad', (err, data) => console.log({ err, data }))
myAction('good', (err, data) => console.log({ err, data }))

Example:

const myAction = (input) =>
  input !== 'good' ? { err : Error('uh oh') } : { data: 'YES!' }

myAction('bad') //=> { err: [Error: uh oh] }
myAction('good') //=> { data: 'YES!' }

The benefits are:

  • All return types are handled the same way. (with throw you have to handle an exception being returned with a try-catch).
  • Exceptions are a valid result of a function. This forces the user of the function to be aware of such return values.
Thread Thread
 
qm3ster profile image
Mihail Malo • Edited

Yeah, Result<Err, Val> is an awesome datatype, but it's not the norm in JS, unfortunately.
How great would Promise<Err, Val> = Future<Result<Err, Val>> be :)

Instead, they integrated with exceptions, which makes throwing inside an async function great, but makes awaiting a fallible promise pretty ugly.

Thread Thread
 
joelnet profile image
JavaScript Joel

That's the difficulty is going opposite of the norm. It really works out well when your entire application is designed that way, but can be odd when you inject it into an existing application.

Consistency across the application is more important. So I will do these things with a new app, but code in the same style as existing apps just for consistency.

Thread Thread
 
qm3ster profile image
Mihail Malo

Look what I found.

Thread Thread
 
joelnet profile image
JavaScript Joel

This guy knows what up ;)

Collapse
 
samuraiseoul profile image
Sophie The Lionhart

I like using short-circuiting for things like that instead. With the nested ternary its hard to see what part is in what part.

function(field1, field2, field3) {
    field1 || Promise.reject('No field 1!')
    Array.isArray(field2) || Promise.reject('Field 2 is not an array!')
    isValidType(field3) || Promise.reject('Invalid field 3!')
}
Collapse
 
avalander profile image
Avalander

But that function doesn't return anything, how do you get a return value?

Collapse
 
samuraiseoul profile image
Sophie The Lionhart

I didn't actually read your function that carefully, but since it looks like it either rejects or does a Promise.resolve() then just at the end add return Promise.resolve() since nothing seems acted upon.

Thread Thread
 
avalander profile image
Avalander

I'm sorry but I still don't get how this is supposed to work. The statement field1 || Promise.reject(...) will not make the function return, so it will return Promise.resolve() every time.

I can see it working if you wrap the body in a new promise

const validateInput = ({ field1, field2, field3 }) => new Promise((resolve, reject) => {
  field1 || reject('Nope')
  Array.isArrat(field2) || reject('Double nope')
  resolve()
})

But I'm not sure this conveys the intent as clearly as a chained conditional operator or if statements.

Thread Thread
 
emgodev profile image
Michael • Edited

I like this much more than nested conditions. And if used appropriately, it's better than my example. It's important to find the right use for each situation.

Thread Thread
 
samuraiseoul profile image
Sophie The Lionhart

I didn't test my code at all and assumed we were doing a basic javascript styled pseudo-code thing here. My bad.

I assumed that basically what you just posted is what we had but we weren't showing the promise set up stuff, just the helper function. But yeah, the example you posted now is what I was going for more or less.

I think the nested ternary is super hard to read, and this is easier, though the use of the short-circuit like this is not a standard thing people do, I've found that people come to like it once they get used to it.

This all said, Typescript could remove the need for all the validation entirely.

Thread Thread
 
avalander profile image
Avalander

Javascript pseudocode is fine, I kept asking because I didn't understand the intent of your code, not because it didn't work.

But yeah, the validations are not important, what I wanted to discuss is the construct in general, my particular case was just an example, so thanks for your input!

Collapse
 
kayis profile image
K

Prettier formats it like that:

const validateInput = ({ field1, field2, field3 }) =>
  !field1
    ? Promise.reject("Missing field1")
    : !Array.isArray(field2)
    ? Promise.reject("field2 is not an array")
    : !isValidType(field3)
    ? Promise.reject("field3 is invalid")
    : Promise.resolve();

Looks okay to me.

This looks a bit more readable to me, but that's just habit, I guess:

const validateInput = ({ field1, field2, field3 }) =>
  new Promise((resolve, reject) => {
    if (!field) return reject("Missing field1");
    if (!Array.isArray(field2)) return reject("field2 is not an array");
    if (!isValidType(field3)) return reject("field3 is invalid");
    resolve();
  });
Collapse
 
cathodion profile image
Dustin King

It seems verbose to have to do if... return.. for each one, but I find this much more readable.

Collapse
 
danjfletcher profile image
Dan Fletcher

Personally, I find chaining ternary expressions like this far more readable than a bunch of if statements :) There's actually a great article on this subject worth reading: medium.com/javascript-scene/nested...

Collapse
 
itsasine profile image
ItsASine (Kayla)

I like multiple conditionals, but I make sure to keep some sort of nesting and breaking strategy to keep it readable. Innately, I can't think of a reason not to use nested other than readability, so if future you will still know what it does, go for it!

Most likely, I would have written that as

const validateInput = ({ field1, field2, field3 }) => (
    !field1 ? Promise.reject('Missing field1') : 
    !Array.isArray(field2) ? Promise.reject('field2 is not an array') :
    !isValidType(field3) ? Promise.reject('field3 is invalid') :
    Promise.resolve()
)

Whatever way works for you to see the ifs and elses without hunting for them.

Collapse
 
mrnoctv profile image
loctv

In general, I will never use nested conditional operator, simply because it looks ugly (to me) and it’s hard to debug. Flatten out your code is the best way for readable and maintainable code.

Collapse
 
cathodion profile image
Dustin King

Indentation that follows the level of nesting would help me understand what is happening:

const validateInput = ({ field1, field2, field3 }) =>
    (!field1
        ? Promise.reject('Missing field1')
        : !Array.isArray(field2)
            ? Promise.reject('field2 is not an array')
            : !isValidType(field3)
                ? Promise.reject('field3 is invalid')
                : Promise.resolve()
    )

I'm not sure I got the evaluation order right, and I'd be suspicious about it in somebody else's code. Usually I'd use parentheses to make sure, but that might make it less readable with a lot of nesting.

Collapse
 
ramblingenzyme profile image
Satvik Sharma • Edited

If you want to write this style of code, I'd prefer using cond from lodash which reimplements cond from Lisp.

const validateInput = _.cond([
    [({ field1 }) => !field1, Promise.reject('Missing field 1')],
    [({ field2 }) => !Array.isArray(field2), Promise.reject('field2 is not an array')],
    [({ field3 }) => !isValidType(field3), Promise.reject('field3 is invalid')],
    [_.stubTrue, Promise.resolve()]
]);

Edit: didn't realise it was an object, not an array.

Could even do something like this:

const isMissing = key => obj => !obj[key];
const notArray = key => obj => !Array.isArray(obj[key]);
const notType = (type, key) => obj => typeof obj[key] !== type;

const validateInput = _.cond([
    [isMissing('field1'), Promise.reject('Missing field 1')],
    [notArray('field2'), Promise.reject('field2 is not an array')],
    [notType('string', 'field3'), Promise.reject('field3 is invalid')],
    [_.stubTrue, Promise.resolve()]
]);
Collapse
 
avalander profile image
Avalander

This is a great suggestion, I actually ended up doing something similar with Ramda's cond

Collapse
 
nestedsoftware profile image
Nested Software

Just wondering, how come this code only checks the first invalid input, if they are all independent? Does that mean in this domain it’s not useful to find all of the invalid inputs at once?

Collapse
 
avalander profile image
Avalander • Edited

Let's keep in mind that I was refactoring old code, I wouldn't design a validation system like this to start with. I kept it like this however because I didn't want to change any functionality during the refactor, and this code is in a request handler in a web service and the client can't handle a response with multiple errors anyway.

Collapse
 
nestedsoftware profile image
Nested Software • Edited

No worries. In that case I think I’d prefer just (pseudocode):

if (invalid1)
    return error1
if (invalid2)
    return error2
etc...

I think it’s more straightforward.

Update:

The use of the ternary operator for this seems possible, but in my opinion it's leaning in the direction of being cute for cute's sake rather than really being useful. At the very least I would argue it's not idiomatic js, and if there is a benefit to it at all, I don't think it's significant enough. In general I like the idea of using the idiomatic approach for a given language unless there is really quite a strong reason not to.

All that being said, if you and your team are comfortable with this code, I don't think using it is the end of the world.

Collapse
 
marcel_cremer profile image
Marcel Cremer

In my opinion the main problem in this particular case is not the tenary operator - it's the ugly code that results from the violation of the DRY principle at the "error throwing".

Let's break this down a bit:

  • You have several "I validate a field"-things going on.
  • each of them will reject the "overall"-Promise, when the validation is not valid.
  • if every field validation is successfull, the Promise is resolved.

Why don't you just tell your program exactly this idea?

"I validate a field" becomes a function, that rejects a Promise on error, or resolves it if everything is fine:

// async can be Replaced with new Promise(bla)
validateField = async (condition, message) => {
    if(!condition)
        throw new Error(message);
}

Now we can set a condition and get a resolved / rejected Promise back - we just need to execute all of them (with Promise.all) and use the overall success as indicator, that all validation was successful:

const validateForm = ({ field1, field2, field3 }) => Promise.all([
    validateField(!!field1, 'Missing field1'),
    validateField(Array.isArray(field2), 'field 2 is not an Array'),
    validateField(!isValidType(field3), 'field3 is invalid')
]);

I think that this increases readability by several levels and tells us on the first glance, what it's doing. You might even add other fancy stuff in the "validateField"-Function, which applies to all validations, or you can use the "validateField"-part without calling the validateForm-Function.

I think if I found your piece of code, I would've refactored it like that and I can imagine, other places where "multiple" tenarys would be needed can be refactored like that as well.

What do you think about this attempt?

Collapse
 
nestedsoftware profile image
Nested Software • Edited

validateField seems reasonable, but I am not sure why it is asynchronous. All of the field information seems to be available as-is, so it looks as though validateField and validateInput could be simplified:

const main = () => {
    const obj = {
        field2: [1,2,3],
        field3: 'joe'
    }

    result = validateInput(obj)

    result
        .then(success=>console.log('validation was successful'))
        .catch(error=>console.log(`${error}`))
}

const validateInput = ({field1, field2, field3}) => {
    try {
        validateField(exists(field1), 'Missing field1')
        validateField(Array.isArray(field2), 'field 2 is not an Array')
        validateField(isValidType(field3), 'field3 is invalid')

        return Promise.resolve()
    } catch (error) {
        return Promise.reject(error)
    }
}

const validateField = (condition, message) => {
    if (!condition)
        throw new Error(message)
}

//implemented as a stub
const isValidType = field => true

//not sure if there needs to be a distinction between boolean
//fields that are set to false, fields that are set to null or empty string, 
//and fields that simply don't exist in the object
const exists = field => field != undefined

main() //Error: Missing field1

Also, I'm not sure why there are negations in !!field1 and !isValidType(field3).

Collapse
 
marcel_cremer profile image
Marcel Cremer

I just made it async, because I was too lazy to write new Promise bla (async always returns a Promise). Also I used Promise.all because I guessed that Avalander had some fancy async validation stuff going on.

!!field1 casts a value to a boolean (a bit hacky, I know 😉 Just wanted to point out, that someone should provide a boolean value).

!isValidType is taken from the original post - of course non-negated conditions should be preferred.

Thread Thread
 
avalander profile image
Avalander

Yeah, let's not forget that the code I posted is a simplification of the real problem, but we can ignore the asynchronous part, I just wanted to discuss the chained conditional operators.

Collapse
 
avalander profile image
Avalander

Using Promise.all is a very interesting approach, I like it.

Collapse
 
madhadron profile image
Fred Ross

I think that if I were to run across this while digging through code, I would be much happier to just see

const validateInput = ({ field1, field2, field3 }) => {
    if (!field1) {
        return Promise.reject('Missing field1');
    }
    if (!Array.isArray(field2)) {
        return Promise.reject('field2 is not an array');
    }
    if (!isValidType(field3)) {
        return Promise.reject('field3 is invalid');
    }
    return Promise.resolve();
}

If definitely doesn't win any code golf or cleverness competitions, but it imposes an absolute minimum of cognitive load on someone passing through the code.

Collapse
 
brunods profile image
bruno da silva

I don't know for js but in PHP, the evaluation is non trivial (left to right):

stackoverflow.com/a/6224398

Collapse
 
qm3ster profile image
Mihail Malo • Edited

I would format it like this:

const validateInput = ({ field1, field2, field3 }) =>
  !field1
    ? Promise.reject('Missing field1')
    : !Array.isArray(field2)
      ? Promise.reject('field2 is not an array')
      : !isValidType(field3)
        ? Promise.reject('field3 is invalid')
        : Promise.resolve()

To my disappointment, Prettier does exactly that when used to format selection but flattens it when using format document. What gives?!
Disclaimer: it originally didn't get rid of the parentheses, but after I removed them it didn't reinsert them.

However, you are messing around with Promises every single return here, so maybe the real solution here is the an async function?

const validateInput = async ({ field1, field2, field3 }) => {
  if (!field1) throw "Missing field1"
  if (!Array.isArray(field2)) throw "field2 is not an array"
  if (!isValidType(field3)) throw "field3 is invalid"
}

Wow, this is amazing :O And shorter! And will produce less diffs on refactoring!

However, do we actually need to return promises? Surely not!

const validateInput = ({ field1, field2, field3 }) => {
  if (!field1) return "Missing field1"
  if (!Array.isArray(field2)) return "field2 is not an array"
  if (!isValidType(field3)) return "field3 is invalid"
}

// we can get a promise later if we really need to
const asyncValidateInput1 = async obj => {
  const errStr = validateInput(obj)
  if (errStr) throw errStr
}
// and your conditional operators approach
const asyncValidateInput2 = obj => {
  const errStr = validateInput(obj)
  return errStr ? Promise.reject(errStr) : Promise.resolve()
}

But do we ever? Nah.

const assertInput = obj => {
  const errStr = validateInput(obj)
  if (errStr) throw errStr
  return obj
}

getObj.then(assertInput).then(/* safely use `obj` here */)

If someone actually reads this long-azz comment, yes, you should usually wrap what you're rejecting with into a new Error(str), for instance in the assertInput func, but this might be an acceptable exception.

Collapse
 
qm3ster profile image
Mihail Malo

Let me tell you the story of how once upon a time I refactored someone's switch cases to a functional expression with nice declarative map lookups:

Old code:

function typeOfSchemaOld(schema: JSONSchema): SCHEMA_TYPE {
  if (schema.allOf) return 'ALL_OF'
  if (schema.anyOf) return 'ANY_OF'
  if (schema.oneOf) return 'ONE_OF'
  if (schema.items) return 'TYPED_ARRAY'
  if (schema.enum && schema.tsEnumNames) return 'NAMED_ENUM'
  if (schema.enum) return 'UNNAMED_ENUM'
  if (schema.$ref) return 'REFERENCE'
  if (Array.isArray(schema.type)) return 'UNION'
  switch (schema.type) {
    case 'string': return 'STRING'
    case 'number': return 'NUMBER'
    case 'integer': return 'NUMBER'
    case 'boolean': return 'BOOLEAN'
    case 'object':
      if (!schema.properties && !isPlainObject(schema)) {
        return 'OBJECT'
      }
      break
    case 'array': return 'UNTYPED_ARRAY'
    case 'null': return 'NULL'
    case 'any': return 'ANY'
  }

  switch (typeof schema.default) {
    case 'boolean': return 'BOOLEAN'
    case 'number': return 'NUMBER'
    case 'string': return 'STRING'
  }
  if (schema.id) return 'NAMED_SCHEMA'
  if (isPlainObject(schema) && Object.keys(schema).length) return 'UNNAMED_SCHEMA'
  return 'ANY'
}

New code

type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U }
  ? U
  : never
type Strings<T> = Exclude<T, Exclude<T, string>>

type SchemaIdentifier = (schema: JSONSchema) => SCHEMA_TYPE

const propertyMap: [KnownKeys<JSONSchema>, SCHEMA_TYPE | SchemaIdentifier][] = [
  ['allOf', 'ALL_OF'],
  ['anyOf', 'ANY_OF'],
  ['oneOf', 'ONE_OF'],
  ['items', 'TYPED_ARRAY'],
  [
    'enum',
    schema =>
      schema.hasOwnProperty('tsEnumNames') ? 'NAMED_ENUM' : 'UNNAMED_ENUM'
  ],
  ['$ref', 'REFERENCE']
]

const defaultMap: Record<string, SCHEMA_TYPE | undefined> = {
  boolean: 'BOOLEAN',
  number: 'NUMBER',
  string: 'STRING'
}

const anyMoreProps: SchemaIdentifier = schema =>
  !schema.properties && !isPlainObject(schema)
    ? 'OBJECT'
    : defaultMap[typeof schema.default] ||
      (schema.id
        ? 'NAMED_SCHEMA'
        : isPlainObject(schema) && Object.keys(schema).length
          ? 'UNNAMED_SCHEMA'
          : 'ANY')

const typeLookup: Record<
  Strings<JSONSchema['type']>,
  SCHEMA_TYPE | SchemaIdentifier
> = {
  string: 'STRING',
  number: 'NUMBER',
  integer: 'NUMBER',
  boolean: 'BOOLEAN',
  object: anyMoreProps,
  array: 'UNTYPED_ARRAY',
  null: 'NULL',
  any: 'ANY'
}

const typeOfSchemaNew = (schema: JSONSchema): SCHEMA_TYPE => {
  const firstPropertyMatched = propertyMap.find(([key]) =>
    schema.hasOwnProperty(key)
  )
  const val = firstPropertyMatched
    ? firstPropertyMatched[1]
    : schema.hasOwnProperty('type') && schema.type
      ? Array.isArray(schema.type)
        ? 'UNION'
        : typeLookup[schema.type]
      : anyMoreProps
  return typeof val === 'string' ? val : val(schema)
}

👍 👍 👍 Too bad it was rejected 👍 👍 👍