-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Control flow based type narrowing for assert(...) calls #8655
Comments
I feel like this should work: function assertIdentifier(n: Node): (n is Identifier) | never But #5992 has made this impossible, by only allowing type predicates to be used as return types. |
I like @isiahmeadows' idea in #12885. With that, declare function assertIdentifier(n: Node): n is Identifier;
declare function assertIdentifier(n: Node): never;
let x: Node = ...
assertIdentifier(x);
x; // CFA infers x is an Identifier here, since the other overload declares it never returns |
@yortus That wouldn't work, because Regarding the function overload idea, I've developed that idea further into something far more broadly useful: #13257 Here's how it'd apply here: declare function assertIdentifier(n: Node): [
case n is Identifier: void,
default: throw,
]; (n.b. The |
@isiahmeadows right, my snippet above based on existing syntax (i.e. without constraint types) should be: declare function assertIdentifier(n: Identifier): void;
declare function assertIdentifier(n: Node): never;
let x: Node = ...
assertIdentifier(x);
x; // CFA infers x is an Identifier here, since the other overload declares it never returns |
Any updates on this? I would love to see the chai equivalent interface myTypeA {
typeGaurd: "myTypeA";
myValue: Boolean;
}
interface myTypeB {
typeGaurd: "myTypeB";
}
// ...
const myObj: myTypeA | myTypeB = getMyObj();
expect(myObj.typeGaurd).to.equal("myType"); // type guard assert
expect(myObj.myValue).to.be.true; // No error :) |
@johnemau Chai's type definitions currently suck as-is, and they really need rewritten to not use So that's equally a failing of that library, and you wouldn't see results until that's fixed. |
As for this request, here's my thought of what a proposal could look like:
|
Edit:
|
For me, the most important aspect here is for there to be some way to write a generic I'm starting to move my team's large JS codebase to TypeScript, and we have an Concrete examples of where TypeScript could do better: assert(this.props.synthesisRulesRun != null);
const violations = cutSiteViolations(this.props.synthesisRulesRun, enzyme); if (!assertAndContinue(field.requiredLink, `Field with id=${field.id} has no requiredLink`)) {
return [];
}
const linkType = field.requiredLink.sampleType; Both of these could be solved with the |
@alangpierce In our big TypeScript project we use the following helper: export function assertExists<A>(value: A | null | undefined): A {
if (value != null) {
return value;
} else {
throw new Error("Value doesn't exist");
}
} You can use it like this: const synthesisRulesRun = assertExists(this.props.synthesisRulesRun);
const violations = cutSiteViolations(synthesisRulesRun, enzyme); It would be ideal for TypeScript to have better support for generic |
It's a shame that return types from methods can't be used for performance reasons. Having throw expressions still require two separate methods if both validation and error message is not triial and needs to be repeated: if (!isCircle(x)) throw incorrectTypeError(x); assertIsCircle(x); But I understand that the trade-off might not be worth it. |
This would be wonderful - at the moment I'm doing something like this with JSON imports: import { Foo, Bar } from './types.ts'
import { assertFoo, assertBar } from './assert.ts'
import * as fooJson from './foo.json'
import * as barJson from './bar.json'
assertFoo( fooJson )
assertBar( barJson )
export const foo = <Foo>fooJson
export const bar = <Bar>barJson But it would be great if I could just do this, and the exported types would be import { assertFoo, assertBar } from './assert.ts'
import * as foo from './foo.json'
import * as bar from './bar.json'
assertFoo( foo )
assertBar( bar )
export { foo, bar } |
Response to #8655 (comment)
This is not alternate solution. For example, in If control flow analysis already understands user defined type guards, why do not add another special form for assert like functions? |
Slightly out-there suggestion from someone not experienced with TS internals, but if the problem is that the graph is built with syntax, how about adding an assert typeof s === "string";
console.log(s.length); |
That is counter to the design goals of TypeScript:
|
@kitsonk too bad about that number 8, otherwise just adding an |
You could make an identity function with a built in assertion, if you're willing to re-assign the variable. const foo = (a: number | null) => {
a = shouldBe(_.isNumber, a)
a // a is number
}
const shouldBe = <T>(fn: (t1) => t1 is T, t) => (fn(t) ? t : throwError(fn, t))
const throwError = (fn:Function, t) => {
throw new Error(`not valid, ${fn.name} failed on ${t}`)
} where |
Implementation now available in #32695. |
Now that TypeScript does control flow based type analysis, and there is a
never
type in the works, is it possible to consider providing better type checking aroundassert(...)
function calls that assert that a variable has a certain type at runtime?TL;DR: some
assert
functions are really just type guards that signal viareturn
/throw
rather thantrue
/false
. Example:Problem
Asserts are common in contract-based programming, but I've also been coming across this scenario regularly whilst traversing JavaScript ASTs based on the Parser API (I'm using babel to produce/traverse ASTs).
For example, consider the
MemberExpression
:Note we can assume
property
is anIdentifier
ifcomputed===false
. This is what I'd like to write:Unfortunately that doesn't compile, because
expr.property
does not get narrowed after theassert(...)
call.To get the full benefit of control flow analysis currently, you have to expand the assert call inline:
While preparing the typings for
babel-core
,babel-types
and friends, I noticed that using asserts this way is the norm.babel-types
actually provides anassertXXX
method for everyisXXX
method. TheseassertXXX
functions are really just type guards that signal viareturn
/throw
rather thantrue
/false
.Possible Solutions?
Not sure if it's feasible at all! But the new work on
never
in #8652 suggests a few possibilities.Specific assertions: assertIsT(...)
The compiler would reason that if this assert call returns at all, then it can safely narrow the variable type in following code.
General assertions used with type guards: assert(isT(...))
The more general
assert(cond: boolean)
function would need a different approach and might not be feasible, but here's an idea:For that second
assert
overload to work, the compiler on seeingassert(isT(x))
would have to somehow forward thex is T
narrowing from theisT(x)
expression to theassert(...)
expression at compile-time.Would be great if it also detected/handled things like
assert(typeof x == 'string')
.Not sure if any of this would meet the cost/benefit bar, but it's just an idea.
The text was updated successfully, but these errors were encountered: