Description
🔍 Search Terms
inferred type predicates, satisfies, return type
✅ Viability Checklist
- 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 isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
I would like to allow type predicates to be inferred when the returned value is constrained by a satisfies boolean
expression.
Concretely, I think this code should be legal:
const a: Array<number> = [1, 2, null, 3].filter(x => (x != null) satisfies boolean)
📃 Motivating Example
I am a big fan of explicitly typing any and all function declarations. So, I would never omit the return type on a function like
function isEmptyString(x: unknown): x is '' {
const rv = x === '';
return rv;
}
However, with inferred type predicates, it's actually safer to omit the return type, since user-defined type predicates can be totally unsafe:
function isEmptyString(x: unknown): x is '' {
// whoops
const rv = x !== '';
return rv;
}
In other words, the safer code is
function isEmptyString(x: unknown) {
const rv = x === '';
return rv;
}
However, this is no longer visually explicit about what it's returning. And what's to stop future-me from making a typo and returning a non-boolean, completely changing the function's applicability.
Therefore, I'd like to be able to write
function isEmptyString(x: unknown) {
const rv = x === '';
return rv satisfies boolean;
}
This is both visually clear and easily analyzable by tooling. For example, at typescript-eslint we'd like to implement typescript-eslint/typescript-eslint#9764, but without having an unresolvable conflict with explicit-function-return-type. Allowing a single-return satisfies boolean
could be a good way to create a statically analyzable exception for explicit-function-return-type that also allows you to enforce writing boolean-returning functions that otherwise would be syntactically eligible for inferred type predicates to be written without the explicit return type. We have precedent for something similar being familiar to the community in typescript-eslint/typescript-eslint#10231, where a satisfies
expression is used to constrain but not change the return type of a function.
💻 Use Cases
- What do you want to use this for?
- code style reasons
- linting support
- Rule proposal: prevent an explicit
: boolean
return type annotation if a predicate return type could be inferred typescript-eslint/typescript-eslint#9764 - also related, it could be a useful suggestion in the discussion here: feat(eslint-plugin): [strict-boolean-expressions] check array predicate functions' return statements typescript-eslint/typescript-eslint#10106 (comment)
- What shortcomings exist with current approaches?
- Cannot satisfy both:
- Ensure that every function declaration's return type is constrained
- Avoid writing explicit type predicates that could be inferred more safely by TS
- corollary for above with function expressions occurs when you want to ensure that you're passing a stricter type than a location requires, a la Enhancement: [strict-boolean-expressions] Check array filter/find predicate functions as boolean locations. typescript-eslint/typescript-eslint#8016
- Cannot satisfy both:
- What workarounds are you using in the meantime?
- Not taking advantage of inferred return types in many cases.
Note - an alternative, more general, approach could be to have a return type syntax like so:
const a: Array<number> = [1, 2, null, 3].filter((x): satisfies boolean => x != null)
This would indicate that the return type is constrained, but will still result in the more specific return type. This might also be useful in cases where a function is passed to a generic, or allowing a single satisfies
constraint to apply to multiple return points, like
function returnsOneOrTwo(): satisfies number {
if (Math.random() > 0.5) {
return 1;
} else {
return 2;
}
}
However, a) I don't think these are mutually exclusive, and b) the proposed solution does not require new syntax, whereas the alternate solution does.