-
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
TS 3.7: unlike x is T
, asserts x is T
cannot close over generics defined in outer scopes
#34596
Comments
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 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 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.
1. Literal is the simplest example, for complex ones it would be really
hard for consumers of the API to know what to put there
2. That last type annotation will make it impossible for this system to
work OOTB in ts-checked Javascript files.
Op zo 20 okt. 2019 01:29 schreef Joe Calzaretta <[email protected]>:
… This is, unfortunately, working as intended as per #33622
<#33622>, see #33580
<#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")
Playground Link
<http://www.typescriptlang.org/play/?ts=3.7-Beta#code/JYOwLgpgTgZghgYwgAgDLDAHgCrIgD0hABMBnZAawgE8B7GZOEagPmQG8AoZH5YUgBQA3OABsArhABcjZgEoZIiSn7JsAbm684pUtDDCxkmU2oLGu-eSWS+5DZwC+nTjHEgEYYLRDJRGaDEcPEIIEnIqOgZTFgF-MBlsc3QsbDYuXmQoCDBxKF8QcVFRC1lqTWdOBB9SMDsACWAZFMwAIgALYFa2AF4-AKgxAQ6uuRdqkFrkfBl3ChBaAHdfPtbIWtaXYAYBfkaAOn4BfDk5Di0eCdJaUQh90VoAc2OzgHpX6btkaqhsz1FqHwQDBoNliMgwLRkAByTrQpxbUgHHR6KAGE6aKo1G53B7PDHId6fUjtWhFcEAIxUwNBEHBOhhcOQtCE0GQ7WgECAA>
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
<#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 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 |
This is consistent in that |
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. |
Usage of parser.validate is unclear because you need to assign an output type, which defeats the purpose of this whole library (see microsoft/TypeScript#34596 and related issues.) This changes .validate (which throws on invalid input) to .validates, which is a type guard that returns false on invalid input. .parse still throws on invalid input. BREAKING CHANGE: TsjsonParser.validate renamed to TsjsonParser.validates and functionality changed from assertion function to type guard.
…rd (#13) Usage of parser.validate is unclear because you need to assign an output type, which defeats the purpose of this whole library (see microsoft/TypeScript#34596 and related issues.) This changes .validate (which throws on invalid input) to .validates, which is a type guard that returns false on invalid input. .parse still throws on invalid input. BREAKING CHANGE: TsjsonParser.validate renamed to TsjsonParser.validates and functionality changed from assertion function to type guard.
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 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 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 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 import { Number, Array } from 'runtypes'
const ArrayOfNumbers = Array(Number)
Number.assert(x) // works!
ArrayOfNumbers.assert(x) // fails! |
@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 |
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 |
The current signature of the abortUnless method doesn't work. Here is a related typescript issue for it microsoft/TypeScript#34596
Came here also wanting to work with 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')
}
}
export type Asserter<T> = ReturnType<typeof getAsserter<T>>; Then in some other module: const Foo = t.type({foo: t.string})
type IFoo = t.TypeOf<typeof Foo>;
const assert: Asserter<IFoo> = getAsserter(Foo);
assert(foo);
foo.foo // ok
// @ts-expect-error :: invalid property
foo.bar // not ok EDIT: Realised the above code still isn't ideal, because you can instantiate |
TypeScript Version: 3.7.0-dev.20191016
Search Terms:
asserts, asserts return, asserts higher order, asserts type, 2775
Code
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
andmobx-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 nottype.assert
, although they seem to be needing the exact same type / depth of type analysis; if type guards can close overT
, so should type assertions?Playground Link: link
Related Issues:
#34523
The text was updated successfully, but these errors were encountered: