Repository navigation
Control flow based type narrowing for assert(...) calls #8655
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScript
on May 18, 2016 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,
assertIdentifiercould be declared as two overloads: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
Troy Gerwien (@yortus) That wouldn't work, because
n is Identifieris just a boolean subtype that narrows withinif-elsewhen it's the raw operand. In fact, that'd work closer to Lucas Neiva (@chilloutman)'s idea than you might expect.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
default: throwis redundant, just provided here for clarity.)@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 :)
John (@johnemau) Chai's type definitions currently suck as-is, and they really need rewritten to not use
anyso much. Twice, now, I migrated a TS project's tests toclean-assert(shameless plug: I wrote it), which has sane type definitions, and it took a solid half a week to fix all the resulting type errors in it. (It had a few thousand tests, and some pretty complex types, too.)So that's equally a failing of that library, and you wouldn't see results until that's fixed.
Reacted by Michael Barney, JrAs for this request, here's my thought of what a proposal could look like:
- This return-specific syntax would be better IMHO:
assume n is Identifier, .... This allows multiple variables to be checked simultaneously. - A function with an
assumereturn type must not return a value. - If a function is otherwise inferred as returning
voidand any argument is narrowed to only one type at all return points, infer the return type toassumethe argument is that type, rather thanvoid. - The return type should be treated as an unconditional type narrowing in the body.
- This return-specific syntax would be better IMHO:
Edit:
assumetypes really should look like this, with a few clarifications/edits:- Syntax:
assume T if n is Identifier, ...Tis any valid return type- Stuff like
assume n is string if n is stringand similar are equivalent totrue.
- Flagged: If a function has an inferred return type, then the inferred return type also
assumes any arguments narrowed if they are narrowed at all return points. - Normal function return rules apply as if the enclosing
assumedidn't exist
- Syntax:
alangpierce commented
on Mar 16, 2018 ContributorMore actionsFor me, the most important aspect here is for there to be some way to write a generic
assertfunction and similar functions, not just type assertions specifically.I'm starting to move my team's large JS codebase to TypeScript, and we have an
assertfunction that we use regularly for things like null checks. We also have anassertAndContinuefunction that crashes in development and returnsfalseand logs an error in production when the condition fails.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
!non-null assertion operator, and most other examples I could find were just null checks like these, but it still would be nice if people on my team could feel like they aren't losing anything by usingassertorassertAndContinuerather than writing extra code inline.Reacted by Zbigniew Zagórski, Kim Knudsen and Chris KrychoAlan Pierce (@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
assert, but the above is a good workaround in the meantime.Reacted by Alan Pierce, Zbigniew Zagórski, Ivan Akulov, Ghabriel Nunes, SlurpTheo and Spencer BlivenJust published
ts-assert-existsbased on the Pauan’s code snippet:import assertExists from 'ts-assert-exists'; const twitterToken: string = assertExists( process.env.TWITTER_TOKEN, 'Twitter token does not exist', );
Reacted by PauanReacted by Pauanalangpierce commented
on May 30, 2018 ContributorMore actionsTo be clear, an
assertExistsfunction works for some cases, but it's not a full solution to this problem. What I'd like is a way to tell TypeScript "if control flow proceeds past this function call, then assume that the argument expression is true", just like it does forifstatements containing areturn.Here's a (simplified) real-world example that I just ran into:
if (!myList || !myList.length) { assert(false, 'Expected nonempty list'); return null; } return myList[0];
Ideally, TypeScript would recognize the assert and allow the following code instead:
assert(myList && myList.length, 'Expected nonempty list'); return myList[0];
Technically you could use
!as a concise way to override TypeScript, but ideally you wouldn't have to.assert(myList && myList.length, 'Expected nonempty list'); return myList![0];
Reacted by Bao Bo, Chris Krycho, Florian Schäfer, Tony Xiao, Suguru Inatomi, Ivan Akulov, Turadg Aleahmad and interphxJust to add my two-cents, while this was not a pattern that I used previously, I am seeing that this pattern is widely adopted and certainly limits the effectiveness of CFA. I don't think it is actively on the roadmap, but it certainly would be a really useful feature to a lot of code bases.
Reacted by Alan Pierce, Bao Bo, Ruslan Fadeev, Tony Xiao, interphx and Ethan Resnick3 remaining items
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixedand removedSuggestionAn idea for TypeScriptAn idea for TypeScript
on Aug 13, 2018 RyanCavanaugh commented
on Aug 13, 2018 MemberMore actionsThis is an important scenario, but any fix would be from #10421 given present architectural constraints.
The control flow graph used to determine which expressions change the type of other expressions is constructed syntactically. Later, typechecking informs how that graph influences expressions. Adding new nodes in this graph is not cheap (in terms of memory/performance), and realistically we couldn't possibly add all function calls to the graph and still achieve reasonable performance.
https://github.com/tc39/proposal-throw-expressions is actually an alternate solution here as well - once that proposal is through, code like
(typeof x === "number") || throw "Wrong type");might be come idiomatic to validate+typecheck expressions in one statement, and that would have the same effect without any architectural rewrites on our side.Reacted by Adrian Sampson, Denis Sokolov and Anton IvanovReacted by ikokostya, cevek, Egor Blinov and Konstantin PelepelinReacted by Anton Ivanov and Micah ZoltuTony Xiao (@tonyxiao)
No, it does not work today.Ryan Cavanaugh (@RyanCavanaugh)
Throw expressions are side-stepping the issue (there is an explicit throw). It still implies that TS users should avoid Node's assert.Reacted by vemoo and Tanner BennettIt'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.
Reacted by ikokostya, Bao Bo, Charles Samborski and ADoyleThis 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
FooandBarbecause of the assert functions: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)
https://github.com/tc39/proposal-throw-expressions is actually an alternate solution here as well - once that proposal is through, code like (typeof x === "number") || throw "Wrong type");
This is not alternate solution. For example, in
Node.jsprocess.exit()call doesn't throw error, but terminates current execution. In addition, with throw expressions syntax still need to repeat conditional expression and error message in every place of usage.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
assertstatement, similar to Python's? Something likeassert typeof s === "string"; console.log(s.length);
That is counter to the design goals of TypeScript:
- Avoid adding expression-level syntax.
Reacted by Sebastian "Sebbie" Silbermann and Bao BoReacted by Kevin Stenerson and Kuangda HeKitson Kelly (@kitsonk) too bad about that number 8, otherwise just adding an
inlinekeyword would solve this issue no? Then the compiler would just expand the type assertion as if it was written inline by the programmer, and everything would work.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
_.isNumberhas a type guardx is numberReacted by Dylan R. Johnston- addedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Aug 3, 2019 Implementation now available in #32695.
Reacted by Adrian Sampson, Titian Cernicova-Dragomir, Mattias Buelens, Steven, 神楽坂 静瑶, Zbigniew Zagórski, Nik , Zhongliang Wang, Anthony Ciccarello, Adrien Turiot and 16 moreReacted by yxliang, Titian Cernicova-Dragomir, Mattias Buelens, ikokostya, Steven, Zbigniew Zagórski, stephen, wyqydsyq, Alberto Leal, Maciej Holyszko and 1 moreBeing fairly new to TypeScript I explored this problem a bit from the
expectsyntax perspective. Thanks Orta Therox (@orta) for pointing me to this issue. Watching.
Now that TypeScript does control flow based type analysis, and there is a
nevertype 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
assertfunctions are really just type guards that signal viareturn/throwrather 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
propertyis anIdentifierifcomputed===false. This is what I'd like to write:Unfortunately that doesn't compile, because
expr.propertydoes 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-typesand friends, I noticed that using asserts this way is the norm.babel-typesactually provides anassertXXXmethod for everyisXXXmethod. TheseassertXXXfunctions are really just type guards that signal viareturn/throwrather thantrue/false.Possible Solutions?
Not sure if it's feasible at all! But the new work on
neverin #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
assertoverload to work, the compiler on seeingassert(isT(x))would have to somehow forward thex is Tnarrowing 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.