Skip to content

and keyword in type predicates to chain assertions #54479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 tasks done
WillsterJohnson opened this issue May 31, 2023 · 2 comments
Closed
5 tasks done

and keyword in type predicates to chain assertions #54479

WillsterJohnson opened this issue May 31, 2023 · 2 comments

Comments

@WillsterJohnson
Copy link

Suggestion

🔍 Search Terms

  • predicate
  • union predicate
  • assertion
  • union assertion
  • type predicate
  • combined predicate

Also searched on Google for solutions prior to concluding this doesn't have a workaround which isn't simply chaining separate predicates at the point of use.

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

and operator in predicate expressions to allow making assertions about more than one of the arguments supplied to the function.

function stringNumberBoolean(
  maybeString: unknown,
  maybeNumber: unknown,
  maybeBoolean: unknown
): maybeString is string
  and maybeNumber is number
  and maybeBoolean is boolean {
  return typeof maybeString === "string"
    && typeof maybeNumber === "number"
    && typeof maybeBoolean === "boolean"
}

📃 Motivating Example

It's reasonable to assume that for any given values a and b, the below function check will either log twice, or not log at all;

function and(a, b) {
  return a && b;
}

function check(a, b) {
  if (a && b) console.log("both")
  if (and(a, b)) console.log("both")
}

It's pretty clear to see why; it doesn't matter if you run the && check directly, or if you run it behind a function, the result will be the same. I'll be ignoring the possibility of the values changing, as this is regarding situations such as using only the and function or only directly checking.

In TypeScript, this is not always the case. We can't use console.log because that's runtime, but we can take a look at the types that our variables have. Here, check demonstates two if statements which are functionally identical (again we're considering using only one of these, not both) but have different resulting type inference.

function isString(arg: unknown): arg is string {
  return typeof arg === "string";
}

function bothString(arg1: unknown, arg2: unknown) {
  return isString(arg1) && isString(arg2);
}

function check(a: unknown, b: unknown) {
  if (bothString(a, b)) {
    a; // expected `string`, actual `unknown`
    b; // expected `string`, actual `unknown`
  }
  if (isString(a) && isString(b)) {
    a; // expected `string`, actual `string`
    b; // expected `string`, actual `string`
  }

By changing where the check is run, the result of the check differs.

Instead of this behavior, it would make sense to allow predicates to be combined. The return type of check in this example ought to be some type which asserts that arg1 and arg2 are both strings. Something such as : arg1 is string and arg2 is string.

The reason to not use & or &&, and to instead use and;

  • it's consistent with the written-word style of type assertions in TypeScript, such as is, as, satisfies.
  • & already has meaning in TypeScript, and giving it different meaning depending on context could be confusing to developers, and could be difficult to implement as arg1 is string & arg2 is string could be interpreted as either two predicates or as arg1 is string & arg2 and a syntax error at is string.
  • && is a boolean operation, taking lhs and rhs and producing either true or false. Strictly speaking, this is not what combined predicates are doing; rather they're making multiple type assertions.

💻 Use Cases

I'll save you the explaination of what and how, but I'm creating a tool which imports from a config file, and reads a CLI name from the command line. It needs to know that;

  • the default export of the config file is an object
  • said object contains a key which is the read CLI name
    It doesn't care about branching out into a few possible errors; if it works it works, if it doesn't it doesn't.

It would be nice to have a toolNameInImport function which can provide both predicates. It would improve readability, document what the if statement is checking for, and generally be a QOL improvement. Instead, I'm required to work around the somewhat inconsistent handling of boolean logic regarding predicates and have `if (isStringIndexable(imported) && isKeyof(toolName, imported)), which is less appealing and requires a comment.


This doesn't fit elsewhere, but I wanted to note this here somewhere;
The addition of assertion expression keywords such as this one would fit nicely alongside the recent satisfies keyword, which has greatly improved QOL and the ability to make type-safe assertions regarding the types of variables.

I'm also certain this has been suggested before. Not because I recall seeing an issue regarding this, but because I don't believe that nobody has thought of this before. Unfortunately of the issues I clicked through to based on the titles, none of them appeared to request multiple assertions in a single predicate. I'll be very surprised if this isn't a duplicate, but it's possible even if unlikely.

@WillsterJohnson
Copy link
Author

Wanted to highlight and rule out a potential workaround;

Chatting about this elsewhere, the following workaround was suggested to me

function isCombined(candidate: {a: unknown, b: unknown}): candidate is {a: string, b: number} {
  return isString(candidate.a) && isNumber(candidate.b);
}

Unfortunately, TypeScript doesn't track this back to asserting that the variables passed to a and b properties are themselves a string and a number respectively.

@jcalz
Copy link
Contributor

jcalz commented Jun 1, 2023

Duplicate #26916

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants