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 isnull
orundefined
. (This is what you have proposed.) - Return
defaultValue
only if we attempt to access a property onnull
orundefined
. - Return
defaultValue
if any of the member expressions arenull
orundefined
.
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
.