tc39 / proposal-optional-chaining

https://tc39.github.io/proposal-optional-chaining/
4.94k stars 75 forks source link

operator for asserting “not null” #25

Closed claudepache closed 7 years ago

claudepache commented 7 years ago

This:

a! // throws a TypeError if `a` is null/undefined, no-op otherwise

Use case. From Jul 2017 TC39 meeting notes:

@maggiepint: Concretely, in HR software I was writing I would frequently want the SSN of an employee of a job if the job had an employee, and get an error if there was an employee with no SSN. I need to be able to express it.

Answer:

job.employee?.SSN!

desugars to (ignoring multiple evaluations):

job.employee == null ? undefined
  : job.employee.SSN == null ? (() => throw new TypeError)()
  : job.employee.SSN

Thoughts?

Mouvedia commented 7 years ago

This doesn't belong on this repository.

Thoughts?

May be useful. Is it taken from another language?

ljharb commented 7 years ago

How would that interact with whitespace and thus ASI?

claudepache commented 7 years ago

Is it taken from another language?

In Swift, see [1] (section “The init! Failable Initializer”) and [2] (section “Optional Chaining as an Alternative to Forced Unwrapping”)

ljharb commented 7 years ago

For this specific use case, would you not chain and call a string prototype function, which would give you a type error if it were not a string?

claudepache commented 7 years ago

How would that interact with whitespace and thus ASI?

Because of ASI, LineTerminator is forbidden before a postfix “!”, so that the following code will not change semantics:

foo
!(bar)

I haven’t found other issue.

Mouvedia commented 7 years ago

Personally I immediately thought of Ruby's dangerous methods. It shares this convention with Scheme.

By convention, the names of procedures that store values into previously allocated locations usually end in !.

claudepache commented 7 years ago

For this specific use case, would you not chain and call a string prototype function, which would give you a type error if it were not a string?

Yes, job.employee?.SSN.valueOf() will work. But what if you expect an object and don’t want to chain... augment your class with a self accessor property, maybe?

ljharb commented 7 years ago

Perhaps:

const maybeObject = job?.employee.SSN;
const importantProperty = maybeObject.something; // will throw if `maybeObject` is == null
// if it has no important properties, then why is it an object?

It might be useful in general to have some kind of "assertion" operator, despite explicit exceptions often adding clarity, but regardless I think it'd be entirely distinct from this proposal.

claudepache commented 7 years ago

Perhaps:

const maybeObject = job?.employee.SSN;

Did you mean job.employee?.SSN?

const importantProperty = maybeObject.something; // will throw if `maybeObject` is == null
// if it has no important properties, then why is it an object?

It won’t work, because you don’t make the distinction between “there is no employee for the job” (which is fine) and “there is no SSN for this employee” (which is not).

It might be useful in general to have some kind of "assertion" operator, despite explicit exceptions often adding clarity, but regardless I think it'd be entirely distinct from this proposal.

However, the particular case is related, because, as the example shows, it complements optional chaining as it concerns specifically the case “== null”, with the opposite sense than “?.”

What I have specifically in mind with !, is that the use of ?. should not make code less safe against bugs. (It is in no way intended as a make-error-silent operator.) In details:

(A) Original code:

if (job.employee != null) {
     let SSN = job.employee.SSN
    // continue, with an implicit assertion that SSN is not null if employee is not null.
    // If it is null, a TypeError will probably rapidly occur.
}

(B) Code using Optional Chaining, less robust:

let SSN = job.employee?.SSN
if (SSN != null) {
    // continue, as above; but you have lost the implicit assertion
}

(C) Code using Optional Chaining, as robust as (A):

//explicit assertion that SSN is not null if job.employee is not null
let SSN = job.employee?.SSN!
if (SSN != null) {
    // continue, as above
}

Maybe it is not necessary to resolve that issue in the scope of the present proposal. But it is good to be at least aware of it.

Mouvedia commented 7 years ago

Maybe it is not necessary to resolve that issue in the scope of the present proposal.

claudepache commented 7 years ago

Just for reference, TypeScript has a similar feature (search for “type assertion operator”).

(In reality, not the same meaning, see comment below.)

tvald commented 7 years ago

TypeScript's type assertion operator is a type hint for the compiler, not a runtime-check assertion. The two are incompatible.

In cases where the compiler can’t eliminate null or undefined, you can use the type assertion operator to manually remove them.

saschanaz commented 7 years ago

a! in TS anyway means that the author is 100% sure that a is not null/undefined. I think the core intention of the proposed operator here is same.

littledan commented 7 years ago

! has been proposed for having something to do with pattern matching/fallible patterns; we should think carefully about tradeoffs for whether we're foreclosing on those opportunities before using it here.

claudepache commented 7 years ago

I’m not pursuing this idea in the scope of this proposal.

Also, once nullish-coalescing is implemented, there will be a relatively reasonable alternative:

var global = Function("return this")()
Object.defineGetter(global, 'FAIL', { 
    get: function() { throw new TypeError("Unexpected null") }
})

a ?? FAIL
ljharb commented 7 years ago

In the future perhaps, a ?? do { throw new Error() }

claudepache commented 7 years ago

@ljharb Even a ?? throw new TypeError per Throw expressions proposal.