Non-null assertion operator (aka Swift's force unwrap)

Interesting, I hadn't thought of other type assertions, but that makes sense. I would however prefer the TypeScript as type assertions keyword.
however, right now I see 3 major deviations:

  • primitive assertions would be done through the string result value of typeof, instead of the non-quoted "primitive type" that TypeScript uses. Worse, a quoted string is a valid type in TS, it's a narrowed string type of a specific value.
const tsNum = getNum() as number; // `number` isn't valid JS
// vs
const jsNum = getNum() as 'number'; // TypeScript consider this to be the `'number'` subtype of string
  • the right hand side would have to be an expression in JavaScript, which isn't currently supported in TypeScript
  • TypeScript supports other types (e.g. any or never) and syntax for them (e.g. Map<Foo>) which aren't valid JavaScript.

If we could end up making the typescript assertions a superset of JavaScript ones, then it might be possible. It'd be a breaking change for typescript though:

  • expressions in the right hand side would have to resolve the type of the expression and consider it as the "class" of.
  • val as 'number' for primitive assertion would be supported
  • val as type 'stringValue', which would be removed at compilation, for explicit type only assertion, and provide an upgrade path for existing TS code.

One assertion that is unclear in JacaScript would be val as null. On one hand it'd be a way to provide a "null assertion" which doesn't have its own typeof, on the other it opens the door to the right hand being an expression that evaluates to null, and that not being in itself a TypeError.

You're proposing we add this "as" type assertion, have TypeScript adopt the semantics via some breaking change, and let legacy TypeScript code switch to using "as type"?

Why not choose a different operator from the start that doesn't conflict with TypeScript's as, which we use for runtime type assertions, and let TypeScript add typing behavior to this new operator, and let legacy code uses TypeScript's existing "as" operator?

Correct, because as is a keyword reserved in places where an identifier is not expected (which would be the case here), and it has the right meaning. There are no other reserved keyword that would work.

It would also be compatible for most existing usages in TypeScript. TS could have heuristics that infer from the invalid usages where no confusion is possible, and issue a warning, or error if there is a confusion. This also wouldn't be the first breaking change that JavaScript changes has on TypeScript.

Depending on how as is specced, TS could also add a runtime shim to make usages like as number be valid. E.g. if it was following instanceof semantics of checking Symbol.hasInstance, you could add a global const number = { [Symbol.hasInstance](value) { return typeof value === 'number'; } } to make 42 as number work.

How is this better than

export function assert(predicate, message) {
  if (!predicate) {
    const error = new Error(message)
    error.name = 'AssertionError'
    throw error
  }
}

I agree best to leave assertions discussions out of this discussion. But to be a hypocrite, the main benefit of a native assertion function is that it would preserve call stacks. (Not sure if there is a call stack workaround?)

I think another common use case is duck type checking, ie check that an object has a property but I guess that can be covered by

value?.foo!

All that said, just foo! is a great start.

Maybe rather than "non-null operator", we might want "non-falsy operator", as then it would be easy to cover the latter two with

(e instanceof Error)!
(typeof x === 'number')!

Another option, if we're not trying to constrain ourselves to the way TypeScript does its types, is to rely on pattern matching syntax for the assertion. It's an idea I brought up over here (but with a different purpose in mind). The syntax would basically be this (I tweaked the syntax here to make it similar to the syntax we've already been discussing)

const user2 = user as { name } // works
const user2 = user as { xyz } // throw an error

The RHS is a pattern-matching expression from the pattern matching proposal. If the language also provides some default matchers, it would not be hard to allow null assertions as well.

const user2 = null as Types.notNullish // throws an error
const user2 = user as Types.notNullish // works

Things like instanceof could easily be supported as well, as I explained over there (which I can elaborate on in this thread, if there's interest in going this direction).

Thus, there would be only one fairly simple piece of syntax to provide assertions for all three of these categories - non-nullish, typeof, and instanceof, along with other stuff like object shape.

Wouldn't this fail because null is nullish and not notNullish?

Whoops, you're right. I changed it from "Types.nullish" to "Types.notNullish", but forgot to update the comments. I've fixed it now.

1 Like

I'm confused. If you want to assert that something is non-nullish - and get an exception otherwise - you can already do this either with Object(x) or x.literallyAnything, both of which throw for nullish values. Why do we need new syntax for this?

Object(null) and Object(undefined) returns {}, so those wouldn't do it.

You would have to do maybeNullish.literallyAnything, which works, but you wouldn't be able to use it inline with other actions, for example, a nullish assertion operator would let you do this:

return f(g()!)

The equivalent today would have to be this:

const result = g()
result.literallyAnything
return f(result)

Or, if you actually care about readability, something like this:

const result = g()
if (result != null) throw new Error()
return f(result)

Alternatively, I'm just realizing that if the throw-as-expression proposal ever goes through, then this would be possible.

return f(g() ?? throw new Error())

This is where .literallyAnything would need to be .valueOf(). That would give you back the value for those that support it, which are basically all non-nullish values except those that have it overridden or don't inherit from Object.prototype. That then brings us back to mhofman's example of assertNonNull:

const assertNonNull = Function.prototype.call.bind(Object.prototype.valueOf);

which works for all non-nullish being called as a function using the original Object implementation of valueOf().

That implementation doesn't actually work right either (if you want to utilize its return value). It'll automatically turn primitives into objects, which could have unexpected consequences on your code.

> const assertNonNull = Function.prototype.call.bind(Object.prototype.valueOf);
> const data = [false, false]
> data.every(x => x)
false
> data.every(x => assertNonNull(x))
true
1 Like

Yes, the goal of any type assertion is to pass the original value through if the check passes.

My assertNonNull didn't attempt to faithfully do that, sorry for the confusion. It was meant as an example of the engine performing a non-null check and throwing if not.

As for the stack trace observation, yes indeed a syntax based type assertion would obviously not add a stack entry, and I assume a built-in assert predicate would avoid this as well. However I'll be pedantic and point out that stack traces are not yet part of the spec.

I just want to point out that this concept exists in Dart language. It's called Null Safety and it tells the IDE to treat the variable as if it were not null. If it is null, this is a problem, so an error is thrown. Not quite like the proposal, but close enough.

exactly the same as this proposal

Yes exactly the same as this proposal, I came from Swift which refers to this as force unwrapping and I expected the same behaviour in TS

When I learnt it didn't behave this way I asked mfxb9a4 if he knew more which lead to this question

Maybe not.

I'm not an expert in Dart, but to my knowledge, it's not a check. An error is thrown only if a null value is assigned to a variable that cannot be null. If the non-null assertion were to be assed to JS, there would have to be a check that looks for null values.

In JS, it says "throw if null." In Dart, it says "treat value as if it isn't null."

With GitHub - tc39/proposal-throw-expressions: Proposal for ECMAScript 'throw' expressions this syntax can more-verbosely be achieved with

(val ?? throw new Error());

If it helps, I was just sketching out the problem space and the shape of a solution. :wink:

I was explicitly not proposing any particular syntax, and almost made it deliberately ugly just to emphasize that.

The driving reason for me is that I want stack traces to be in more helpful places. I've already encountered more than my fair share of state bugs where the stack trace was a red herring, and it's always because the data is generated in one location and processed in another.

Having an easy way to check my assumptions when I generate the data would be far more valuable at avoiding bugs than having code throw in a location far away from where source of the bug actually is.

It's also why I want a slightly more generalized typeof/instanceof-aware form and not just this limited version.

1 Like