claudepache/es-optional-chaining

Different default values for different undefined levels

xareelee opened this issue · 2 comments

Please see the discussion in facebook/idx#5 for the reason about why we need different default values for different undefined levels.

In this example:

const result = props.user.friends[0].friends

We have 5 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 need some symbol to define the default values for different undefined levels.

  • ?(...) will provide a default value if the evaluation returns undefined
  • ?> will try to look up a default value in the optional chain
  • ?(...)! will provide a default value only for this evaluation (it will not be looked up for ?>)
  • ? will not try use a default value

For example:

const defaultValue = []
const result = props?(defaultValue).user?>.friends?>[0]?>.friends?>
                     ^^^^^^^^^^^^^^Provide the default values and it can be looked up

We provide a defaultValue to props if it is undefined. If any following evaluation returns undefined and it use ?>, it will look up the default value in the chain.

props == null
  ? defaultValue  // use `?(defaultValue)`
  : props.user == null 
    ? defaultValue  // use `?>` to look up the default value
    : props.user.friends == null 
      ? defaultValue  // use `?>` to look up the default value
      : props.user.friends[0] == null 
        ? defaultValue  // use `?>` to look up the default value
        : props.user.friends[0].friends
          ? defaultValue  // use `?>` to look up the default value
          : props.user.friends[0].friends  // final result

If we use ?(...)!, for example:

const result = props?(defaultValue)!.user?>.friends?>[0]?>.friends?>
                     ^^^^^^^^^^^^^^^Provide the default values and it can NOT be looked up

the defaultValue can't be used in other optional levels (only for its level):

props == null
  ? defaultValue  // only for this level
  : props.user == null 
    ? undefined
    : props.user.friends == null 
      ? undefined
      : props.user.friends[0] == null 
        ? undefined
        : props.user.friends[0].friends
          ? undefined
          : props.user.friends[0].friends

We could provide different default values for the chain:

const defaultFriends = [{ name: "Pavlos"}]
const result = props?(defaultValue).user?>.friends?>[0]?(defaultFriends).friends?>
props == null
  ? defaultValue
  : props.user == null 
    ? defaultValue
    : props.user.friends == null 
      ? defaultValue
      : props.user.friends[0] == null 
        ? defaultFriends // provide a new default value for this level
        : props.user.friends[0].friends
          ? defaultFriends // look up for the default value
          : props.user.friends[0].friends

We could use just ? to not use a default value

// Only using `?>` will try to look up the default value
const result = props?(defaultValue).user?.friends?[0]?.friends?>
props == null
  ? defaultValue
  : props.user == null 
    ? undefined  // will not look up
    : props.user.friends == null 
      ? undefined  // will not look up
      : props.user.friends[0] == null 
        ? undefined  // will not look up
        : props.user.friends[0].friends
          ? defaultValue
          : props.user.friends[0].friends

How about this proposal?

That seems quite complex for me. The goal of this proposal is to have simple, easy to understand semantics. The only real complexity is the short-circuiting behaviour.

But let see how to handle your use cases, using the current ?. operator plus the null-coalescing ?? operator (which I think should be included, see #10):

const defaultValue = []
const result = props?(defaultValue).user?>.friends?>[0]?>.friends?>
                     ^^^^^^^^^^^^^^Provide the default values and it can be looked up
const result = props?.user?.friends?.[0]?.friends ?? defaultValue
const result = props?(defaultValue)!.user?>.friends?>[0]?>.friends?>
                     ^^^^^^^^^^^^^^^Provide the default values and it can NOT be looked up

For this one, the simplest replacement I can think of is:

let tmp = props
const result = tmp == null ? defaultValue : tmp?.user?.friends?.[0]?.friends

I think that we should stick if possible with the “left-to-right evaluation order plus short-circuiting” principle if possible. For example, let say that ! means “stop the evaluation of this expression” (short-circuit); one could write:

const result = (props ?? defaultValue!).user?.friends?.[0]?.friends
// Only using `?>` will try to look up the default value
const result = props?(defaultValue).user?.friends?[0]?.friends?>

This one is more cumbersome; I wonder however whether that situation happens often.

const result = (() => {
    let tmp = props
    if (tmp == null)
        return defaultValue
    let tmp = tmp?.user?.friends?.[0]
    if (tmp == null)
       return undefined
    return tmp?.friends
    if (tmp == null)
        return defaultValue
    return tmp
})()

With the ! introduced above, it could be written as:

const result = ((props ?? defaultValue!).user?.friends?.[0] ?? undefined!)?.friends ?? defaultValue

But it is not clear to me whether your use cases occurs sufficiently often in real life to balances the burden to learn yet another syntax. Do other languages have solutions for your use cases?

Thanks to your suggestion. Because I want to restrict the scope of my work, I won’t include it. More specifically:

Of course, further improvements may be considered later if there are real needs, although I don’t expect to personally pursue the work in that direction. If you have any suggestion, you should make your case on the es-discuss mailing list.