facebook/idx

Default value

Closed this issue · 3 comments

What do you think about having a default value in case there is an error while accessing a property.

Essentially, the function signature might be like:

function idx<Ti, Tv, Td>(input: Ti, accessor: (input: Ti) => Tv, defaultValue: ?Td): ?Tv

And then we could use it as:

const DEFAULT_FRIENDS = [{ name: "Pavlos" }]
idx(props, _ => _.user.friends[0].friends, DEFAULT_FRIENDS)

Which can be transformed to

props.user == null 
   ? props.user 
   : props.user.friends == null 
         ? props.user.friends 
         : props.user.friends[0] == null 
                ? DEFAULT_FRIENDS
                : props.user.friends[0].friends

I actually originally supported an optional third argument, defaultValue. However, I abandoned it because the expected behavior of such an argument turned out to be slightly more complicated than it seems, and I thought it might lead to surprises.

Here are the different potential behaviors for defaultValue (each with different nuances, none of which are obviously correct):

  • Return defaultValue only if the final member expression is null or undefined. (This is what you have proposed.)
  • Return defaultValue only if we attempt to access a property on null or undefined.
  • Return defaultValue if any of the member expressions are null or undefined.

Then, there's the question of whether or not we should return defaultValue if the final value is null or undefined.

The difference between these are pretty subtle and easy to get mixed up. In order to avoid confusion, it would be better for callers to explicitly expand the defaulting logic inline.

Also, we should think of idx as a temporary workaround until we have the real existential operator. A real existential operator will not support default values, so we should not either.

Happy to discuss further, but closing this for now. Thanks for the proposal, though!

In @ppoliani 's example:

const DEFAULT_FRIENDS = [{ name: "Pavlos" }]
idx(props, _ => _.user.friends[0].friends, DEFAULT_FRIENDS)

There are five chances to get undefined:

props == undefined
props.user == undefined 
props.user.friends == undefined 
props.user.friends[0] == undefined 
props.user.friends[0].friends == undefined

We can pass the fourth argument about how to handle undefined. There are 3 values for strategies:

  • 'LOOKUP'
  • 'EXACTLY'
  • 'ANY'

LOOKUP

We can define default values for levels:

const DEFAULT_FRIENDS = [{ name: "Pavlos" }]
const defaultValues = {
  0: [],    // for level 0 to level 3
  4: DEFAULT_FRIENDS  // for level 4
}
const result = idx(props, _ => _.user.friends[0].friends, defaultValues, 'LOOKUP')

So we can get different default values for different undefined levels:

props == null
  ? []  // level 0 default value
  : props.user == null 
    ? []  // level 1 default value
    : props.user.friends == null 
      ? []  // level 2 default value
      : props.user.friends[0] == null 
        ? [] // level 3 default value
        : props.user.friends[0].friends == null
          ? [{ name: "Pavlos" }]  // level 4 default value
          : props.user.friends[0].friends

The 'LOOKUP' strategy uses inheritance, which means that if you did not provide the default value for that level, it will look up the previous level iteratively until finding a default value. If no default value is provided in the inheritance, it will use undefined.

// now we only provide level 4 default value
const defaultValues = {
  4: DEFAULT_FRIENDS
}
const result = idx(props, _ => _.user.friends[0].friends, defaultValues, 'LOOKUP')

// results
props == null
  ? undefined  // level 0 default value
  : props.user == null 
    ? undefined  // level 1 default value
    : props.user.friends == null 
      ? undefined  // level 2 default value
      : props.user.friends[0] == null 
        ? undefined // level 3 default value
        : props.user.friends[0].friends == null
          ? [{ name: "Pavlos" }]  // level 4 default value
          : props.user.friends[0].friends

EXACTLY

When we use 'EXACTLY', it will only use the default value in the exact level:

const DEFAULT_FRIENDS = [{ name: "Pavlos" }]
const defaultValues = {
  0: [],    // only for level 0
  4: DEFAULT_FRIENDS  // only for level 4
}
const result = idx(props, _ => _.user.friends[0].friends, defaultValues, 'EXACTLY')
props == null
  ? []  // level 0 default value
  : props.user == null 
    ? undefined  // level 1 default value
    : props.user.friends == null 
      ? undefined  // level 2 default value
      : props.user.friends[0] == null 
        ? undefined // level 3 default value
        : props.user.friends[0].friends == null
          ? [{ name: "Pavlos" }]  // level 4 default value
          : props.user.friends[0].friends

It won't look up the default value in other levels.

ANY

We provide only a default value for all undefined levels:

const DEFAULT_FRIENDS = [{ name: "Pavlos" }]
const result = idx(props, _ => _.user.friends[0].friends, DEFAULT_FRIENDS, 'ANY')
props == null
  ? [{ name: "Pavlos" }]  // level 0 default value
  : props.user == null 
    ? [{ name: "Pavlos" }]  // level 1 default value
    : props.user.friends == null 
      ? [{ name: "Pavlos" }]  // level 2 default value
      : props.user.friends[0] == null 
        ? [{ name: "Pavlos" }] // level 3 default value
        : props.user.friends[0].friends == null
          ? [{ name: "Pavlos" }]  // level 4 default value
          : props.user.friends[0].friends

If you don't pass the fourth argument to idx(), the default strategy would be 'ANY;, which means that we'll get DEFAULT_FRIENDS for any undefined value.

const DEFAULT_FRIENDS = [{ name: "Pavlos" }]
const result = idx(props, _ => _.user.friends[0].friends, DEFAULT_FRIENDS) // Use 'ANY' strategy

How about this proposal?

@xareelee Thanks for the detailed proposal here and in claudepache/es-optional-chaining#16. However, I personally think it overcomplicates the problem that the optional chaining / existential operator seeks to solve.

Since idx is meant to be a polyfill for the ES proposal, I'll defer discussion there. If it gets incorporated into the proposal and makes reasonable progress in review, we can bring it into idx.