TypeScript Version: 3.7.0-dev.20191016
Search Terms:
asserts, asserts return, asserts higher order, asserts type, 2775
Code
function literal<T extends keyof any>(lit: T): {
is(value: any): value is T
assert(value: any): asserts value is T
} {
return null as any; // implementation doesn't matter
}
const isHi = literal("hi")
const x: unknown = "test"
if (isHi.is(x)) {
console.log(x) // x is correctly inferred to be 'hi' :)
}
isHi.assert(x); // error: Assertions require every name in the call target to be declared with an explicit type annotation.(2775)
console.log(x); // x should be inferred to be 'hi' here :(
Expected behavior:
No compile error, x is inferred to be "hi" on the last line.
Actual behavior:
Compile error on isHi.assert. Assertions require every name in the call target to be declared with an explicit type annotation.(2775)
Construction higher order type guards is possible without problem (as shown in the snippet). This mechanism used heavily in libraries like io-ts and mobx-state-tree.
However, when trying to extend the latter library with assertion functionality for more convenient control flow, we run into this issue.
We can build type.is properly, but not type.assert, although they seem to be needing the exact same type / depth of type analysis; if type guards can close over T, so should type assertions?
Playground Link: link
Related Issues:
This is, unfortunately, working as intended as per #33622, see #33580. The issue is not about closing over generics, but about the fact that your isHi variable is not typed via explicit type annotation. The error message says as much, I guess, but it is bewildering, and I expect you won't be the last person bewildered by it. The fix is something like:
interface Lit<T extends keyof any> {
is(value: any): value is T;
assert(value: any): asserts value is T;
}
function literal<T extends keyof any>(lit: T): Lit<T> {
return null as any;
}
const isHi: Lit<"hi"> = literal("hi")
in which you are forced to write out a type like Lit<"hi">.
This is clearly going to be a pain point with user-defined assertion functions once 3.7 is out of beta. Is the current error message really the best we can do? Anyone have any great ideas? I was hoping the error would come with a "quick fix" that suggests some suitable annotation somewhere, but I guess that would be too hard to get right (if the compiler could always figure out what the annotation should be I guess the annotation wouldn't be necessary in the first place).
That last type annotation is exactly what I want to avoid.
Op zo 20 okt. 2019 01:29 schreef Joe Calzaretta notifications@github.com:
This is, unfortunately, working as intended as per #33622
https://github.com/microsoft/TypeScript/pull/33622, see #33580
https://github.com/microsoft/TypeScript/issues/33580. The issue is not
about closing over generics, but about the fact that your isHi variable
is not typed via explicit type annotation. The error message says as much,
I guess, but it is bewildering, and I expect you won't be the last person
bewildered by it. The fix is something like:interface Lit
{
is(value: any): value is T;
assert(value: any): asserts value is T;
}
function literal(lit: T): Lit {
return null as any;
}
const isHi: Lit<"hi"> = literal("hi")in which you are forced to write out a type like Lit<"hi">.
This is clearly going to be a pain point with user-defined assertion
functions once 3.7 is out of beta. Is the current error message really the
best we can do? Anyone have any great ideas? I was hoping the error would
come with a "quick fix" that suggests some suitable annotation somewhere,
but I guess that would be too hard to get right (if the compiler could
always figure out what the annotation should be I guess the annotation
wouldn't be necessary in the first place).โ
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/microsoft/TypeScript/issues/34596?email_source=notifications&email_token=AAN4NBDWWDKH4YGX36X4N63QPOQ6HA5CNFSM4JCQ6252YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBX7NSY#issuecomment-544208587,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAN4NBDZJW7LRCUNMJFTEDTQPOQ6HANCNFSM4JCQ625Q
.
To elaborate a bit more, how this functionality in general is used, is like:
import { types } from "mylib"
const User = types.Object({
name: types.string,
tags: types.array(types.union(types.literal("happy"), types.literal("sad")))
})
// Later
if (User.is(someData)) {
// someData is correctly inferred to be of type { name: string, tags: ("happy" | "sad")[] }
}
User.assert(someData) // TS Error
This all works splendidly well for typeguards, however, forcing the user to type User explicitly to be able to make asserts as well, partially defeats the purpose of library, as these types get really large in practice (the demo above is still really small).
What is more, to add to the inconsistency (see snippet below), if assertion is extracted as utility function that receives the type as _generic_ argument, it works fine. So it seems that in principle the TypeChecker in principle doesn't have any trouble checking this, but that there is just a hard limit (magic number) put in place somewhere?
type Guard<T> = { is(value: any): value is T }
function assert<T>(t: Guard<T>, value: any): asserts value is T {
// something
}
assert(isHi, x)
console.log(x); // x is correctly as 'hi' over here
_Edit: I am not sure how this relates to CFA; as to infer the type of isHi only the function signatures matter, not the internal flow for how or when that object was constructed?_
What is more, to add to the inconsistency (see snippet below), if assertion is extracted as utility function that receives the type as generic argument, it works fine.
This is consistent in that assert has an explicit type signature. As @jcalz's says, polymorphism is orthogonal to the cause of the issue - though it does exacerbate to pain because instantiated generic types are usually harder to write explicit types for.
The issue here is that we need the information about the assertness of a function early enough in the compilation process to correctly construct the control flow graph. Later in the process, we can detect if this didn't happen, but can't really fix it without performing an unbounded number of passes. We warned that this would be confusing but everyone said to add the feature anyway ๐
I think it would be tractable to produce a quick fix in many places, since we (should?) know the originating declaration, know how to write the type down (if possible) in the context of that declaration.
I don't really understand much of this but I tried this today and couldn't make it work. is the takeaway that assertions won't work with polymorphism?
Ran into this exact thing while trying to see how Superstruct could be made more useful for TypeScript and automatically infer the return types whenever people constructed validations.
@RyanCavanaugh I wanted to add a weird behavior that I encountered about this... It seems like there's a difference in this behavior across package boundaries.
For example, given package 'a' which creates an "asserter":
export interface Asserter<T> {
assert(x: unknown): asserts x is T;
}
export const create = <T>(): Asserter<T> => null as any;
export const number = () => create<number>();
export const Number = number();
If you import it in package 'b' the assertion works only if you use the pre-defined asserter, and don't create a new one from the factory:
import { Number, number } from "a";
{
const x: unknown = 42;
Number.assert(x);
x;
// This one works fine, and `x` is known to be of type `number`.
}
{
const x: unknown = 42;
const N = number();
N.assert(x);
// (method) Asserter<number>.assert(x: unknown): asserts x is number
// Assertions require every name in the call target to be declared with an
// explicit type annotation.ts(2775)
// index.ts(12, 9): 'N' is declared here.
x;
}
Here's the reproduction as a repo to clone.
Even further, if you actually inline the logic from package 'a' as a file inside package 'b' and import it with './a', both of the examples will error.
I kind of assume this is "expected" given the internal limitations of how this feature is written, but externally it feels weird and adds confusion.
For an example of confusion in the wild, there's a library called runtypes which has added an .assert method to its validators, and currently it has this halfway-working behavior. For example:
import { Number, Array } from 'runtypes'
const ArrayOfNumbers = Array(Number)
Number.assert(x) // works!
ArrayOfNumbers.assert(x) // fails!
We can't really _fix_ it without performing an unbounded number of passes. We warned that this would be confusing but everyone said to add the feature anyway ๐
@RyanCavanaugh I won't pretend I fully understand what a "pass" is, but was it considered to "count" the number of "passes" and throw an error similar to the Type instantiation is excessively deep and possibly infinite if there are more than some reasonable limit? I'm not sure whether that would help, I don't know how many passes the examples in this issue would require, for example.
It's possible to workaround this in a strange way, by assigning the assertion function to itself, with a redundant type annotation. Example with io-ts:
import * as t from 'io-ts'
export const getAsserter = <A>(type: t.Type<A>) => (val: unknown): asserts val is A => {
if (!type.is(val)) {
throw Error('Invalid input')
}
}
const _assert = getAsserter(t.type({foo: t.string}))
const assert: typeof _assert = _assert
export const printFoo = (x: unknown) => {
console.log(x.foo) // error: Object is of type 'unknown'
assert(x)
console.log(x.foo) // ok: x now has type {foo: string}
}
๐ the above breaks if you remove the pointless-looking type annotation in const assert: typeof _assert = _assert.
Most helpful comment
The issue here is that we need the information about the assertness of a function early enough in the compilation process to correctly construct the control flow graph. Later in the process, we can detect if this didn't happen, but can't really fix it without performing an unbounded number of passes. We warned that this would be confusing but everyone said to add the feature anyway ๐
I think it would be tractable to produce a quick fix in many places, since we (should?) know the originating declaration, know how to write the type down (if possible) in the context of that declaration.