Skip to content

Question: should (type) assertion functions be "if-and-only-if" or just "only if" #63635

Description

@kirkwaiblinger

Acknowledgement

  • I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.

Comment

Originally posted this question on discord, but was advised by @jakebailey to create an issue here in order to get a response from the TS team.

The question is: is it acceptable to type assertion functions with a type predicate whose converse is not true? I know that boolean-returning type guards must be "if and only if" (along the lines of #15048)

// This is NOT a well-formed type predicate
function isLuckyNumber(x: unknown): x is number {
   return x === Math.random();
}

// because the false branch is unsound
const three = 3 as number | string;
if (isLuckyNumber(three)) {
  three.toExponential(); // safe because of type predicate
} else {
  three.toLowerCase(); // throws at runtime! 
}

However, is it allowed to do a "one-way" type predicate with asserts? For example like so:

function assertIsLuckyNumber(x: unknown): asserts x is number {
   if (x !== Math.random()) {
      throw new Error('you are unlucky today!');
   }
}


const three = 3 as number | string;
assertIsLuckyNumber(three);
three.toExponential(); // safe because of type predicate

// no "false" branch to abuse!

?


The real-world reason I have for asking this question is that typescript-eslint's no-unnecessary-condition currently flags things such as isNumber(num) as "unnecessary", because a well-formed isNumber() type-guard must return true for any value which is already known to be a number.

The current behavior is to also flag assertNumber(num) as "unnecessary", based on the assumption that the asserting type predicate is also "if and only if", and therefore must never throw for any numeric input. However, if an assertion function such as assertLuckyNumber(num) is considered well-formed, that would not be a valid assumption, as the assertion function could still throw even if the input is known to be of type number. If that's the case, no-unnecessary-condition could never consider an asserting type predicate to be "unnecessary".

I came across a usage in the wild that relies on the "only if" (rather than "if and only if") approach in the typings for node's assert.strictEqual().

So, which one is right? Should tooling assume well-formed type assertion functions are "if-and-only-if" or are "one-way" assertion functions considered well-formed? Is the node type "incorrect" or is the typescript-eslint behavior "incorrect"?

Please advise! Thanks!


PS I rather prefer the interpretation that the assertion functions must be "if and only if" in order to be considered well-formed. The phrase asserts x is number should... assert that x is a number. Not "this function may throw even if x is a number, but if it doesn't throw, then x will definitely be a number".

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions