-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Assertion functions don't work when defined as methods #36931
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
Comments
Assertion functions always need explicit annotations. You can write this:
|
@RyanCavanaugh Can you explain in a little more detail? I don't understand how to resolve the issue. This also is failing with the same error: import * as assert from "assert";
export function exists(value: unknown): asserts value {
assert(value);
}
export default {
exists
}; Using: import assert from "./assert";
assert.exists(true); // error ts(2775) How does the exists function not have an explicit annotation? Or what can I do to resolve it in this case. If I just export the function as non-default and do the import as |
I am also seeing error Following has error:
Following has no error
Troubleshoot Info
|
@justinmchase , @amber-pli Create a let/const/var and assign it the assert function you imported. Give that variable an explicit type like in the example provided by @RyanCavanaugh . |
I guess whats confusing me is that he said:
But I believe in my code the function does have an explicit type annotation, yet its not working. Are we saying there is a bug and that the variable assigned to the function needs the explicit type as well? |
@justinmchase The type needs to be explicit, it's not a bug |
Ok, one last time, I swear I'm not being intentionally obtuse but I think I'm homing in. I think my confusion is because, to my understanding, this function has an explicit type: export function exists(value: unknown): asserts value {
assert(value);
} So when you say the type needs to be explicit its boggling my mind because I don't get how it couldn't be explicit. But now, I'm wondering if you don't mean the function's type but the type of the argument needs to be explicit? As in the |
@justinmchase explicitly give it a type where you imported it. |
@justinmchase I think the part that is boggling your mind is that the message isn't clear about what exactly doesn't have an explicit type. You quoted this code, which clearly does have an explicit type:
But when you used it, you imported it like this:
The 2775 error you are getting on that line is not about
So the error isn't telling you about problems with exists, it's telling you that your default export needs to be explicitly typed:
It isn't entirely clear to me why that is the case, and it seems that making the error message clearer about where exactly the problem lies was discussed and rejected. What I have found can work to avoid having to repeat the typings twice is to move the actual assertion functions into a separate file and then re-export them:
I'm also not entirely clear on why this works, but it seems like it does.. |
Ok thanks, I'll try it out! It does seem like there is a bug in here but I'll settle for work arounds for now. |
As a prime example where you would expect this to work, but it doesn't—in a way that seems to me at least to pretty clearly be a bug—is namespaces. For backwards compatibility, Ember's types have both modern module imports and re-exports from the export function assert(desc: string): never;
export function assert(desc: string, test: unknown): asserts test; import { assert } from '@ember/debug';
assert('this works', typeof 'hello' === 'string'); And we also re-export that on the import * as EmberDebugNs from '@ember/debug';
export namespace Ember {
// ...
const assert: typeof EmberDebugNs.assert;
// ...
} const { assert } = Ember;
assert('this FAILS', typeof 'hello' === 'string');
I understand the constraints as specified, and I'm afraid I must disagree with the team's conclusion about this error message—
—seeing as it took me about half an hour to finally find my way to this thread and understand the problem, and another half an hour to figure out how to rewrite our DefinitelyTyped tests to continue handling this! 😅 For the record, if anyone else is curious, this is how I'm ending up working around it for the types PR I'll be pushing up shortly: const assertDescOnly: (desc: string) => never = Ember.assert;
const assertFull: (desc: string, test: unknown) => asserts test = Ember.assert; (I'm also going to have to document that or be ready to explain it, though gladly basically none of our TS users are likely to be doing this.) |
I tried to include an assertion function as part of the exports for an inner module in one of my libraries and was heartbroken to find that, while I could use it perfectly within the module, I could not use it when referencing it from outside via the module namespace. @RyanCavanaugh's answer was a godsend!! Thank you so much! Here's what I was doing: Project1::src/Http.ts export function assertThing(thing: any): asserts thing is string {
// ...
} Project1::src/index.ts import * as Http from "./Http";
// ....
export { Http, /* .... and other things .... */ } Project2::src/Functions.ts import { Http } from "project1";
export const handler = (thing: any) => {
Http.assertThing(thing);
// ....
} I was getting the error in the original post because of this, and I couldn't get past it. I successfully utilized Ryan's solution by doing this: Project2::src/Functions.ts import { Http } from "project1";
const assertThing: (typeof Http)["assertThing"] = Http.assertThing;
export const handler = (thing: any) => {
assertThing(thing);
// ....
} While a little awkward, this is a total life-saver and has allowed me to successfully use this wonderful assertion functionality without limitation. (Note that it was necessary to use the Anyway, just wanted to add this for anyone who may be struggling with something similar. Thanks again for the tip, @RyanCavanaugh! |
* jest is no longer needed * remove ts-check for now until microsoft/TypeScript#36931 or we migrate tests to typescript
For what it's worth this has made the Assert library usable again when checking JavaScript with Typescript.
All the type errors reported about non-explicit types are gone (thankfully). |
I'm using Typescript Here is a sample code: class StringEnum<T extends string> {
public constructor(
private readonly values: readonly T[],
) { }
public is(value: string): value is T {
return (this.values as readonly string[]).includes(value);
}
public assert(value: string): asserts value is T {
if (!this.is(value)) {
throw new Error('invalid string enum variant');
}
}
}
const MyEnum = new StringEnum([
'sample1',
]);
const value = 'sample1';
if (MyEnum.is(value)) { } // works
MyEnum.assert(value); // error From my understanding, the signature of Also, the fact that Since this is a design limitation, it should be forbidden to use |
The error goes away if you're explicit: const MyEnum: StringEnum<string> = new StringEnum([
'sample1',
]); |
Unfortunately that defeats the purpose of the generic argument, which is to keep the valid variant types around. |
Now I'm pretty sure #34596 is about this exact problem too. |
Without being too repetitive, the following works for me: type Assert = (condition: unknown, message?: string) => asserts condition;
const assert: Assert = (condition, message) => {
if (!condition) {
throw new Error(message);
}
};
export default assert; |
I wonder the reasons for it. I think the compiler infers a function/calling signature type rather than an "assertion function type". It explains stuff like this: function assertThing(thing: any): asserts thing is string {
// ...
}
const assertStuff = assertThing; // the constant's type is inferred as `(thing: any) => void` |
Note that if you need method overloading semantics your workaround will need to create an interface:
|
My use case when it breaks: scoped 'logging' utility that adds prefix to logs or asserts simply to save me keystrokes and improve dev-experience when debugging or analyzing complex code scenarios Example: const scope = createScope("Foo");
scope.assert(false, "Nope"); // <— expected: throws error `[Foo] Nope` (saves me `[Foo] ` keystrokes (potentially x20 times in some examples)
scope.log("hello"); // <— expected: logs "[Foo] hello"
// etc. Thus my assert is a method 'by-design' and I want it to be this way. But I cannot use that due to mentioned TS error: scope.assert(false, "Foo"); // TS: Assertions require every name in the call target to be declared with an explicit type annotation.
// Same for
const { assert } = scope
assert(false, "Foo");
// or
const assert = scope;
assert(false, "Foo"); The only way to make it work is: const assert: (input: unknown, message: string) => asserts input = scope.assert
assert(false, "Foo"); // TS does not complain and works
// or
type Assert = (input: unknown, message: string) => asserts input;
const assert: Assert = scope.assert;
assert(false, "Foo"); // I can now re-use `Assert` type, but it is still very manual and misses the point of effective logging / asserting / debugging utility but doing it this way (explicitly having to assign the type to assert totally misses the point which is my case was - make DX of writing debugging/assert messages easier) Also, error message is not really friendly. It is hard for me tu guess what exactly TS wants me to do, forcing to Google it and doing a bit deep read (like this issue) to understand it. It also seems like some internal TS-limitation leaking, rather than some reasonable design decision that makes sense forcing me to not doing what I want to do. Thus something like :
would be way more friendly and save me some time. Or even more friendly: interface Logger {
assert(input: unknown): asserts input; // <— TS Error: Assert functions cannot be part of interfaces
} this way I would know it earlier and in well-targeted place. This way I know my idea of putting assert inside object is not possible, rather than later thinking I use it in a wrong way. |
I am also facing this issue any update when this bug will be resolved? |
Just throwing in my two-cents because I got struck by this annoying and very unhelpful bug wording today.. My 'branding' string code. export type DodiAttributeCodeType = string & { _type: 'DodiAttributeCode' };
/* Delcare type to get around TS Bug */
type DodiAttributeCode = {
guard(str: string): str is DodiAttributeCodeType;
parse(str: string): DodiAttributeCodeType;
assert(str: string): asserts str is DodiAttributeCodeType;
};
export const DodiAttributeCode: DodiAttributeCode = {
guard(str: string): str is DodiAttributeCodeType {
return !!str; /* some complex test */
},
assert(str: string): asserts str is DodiAttributeCodeType {
if (!DodiAttributeCode.guard(str)) {
throw new Error();
}
},
parse(str: string): DodiAttributeCodeType {
DodiAttributeCode.assert(str);
return str;
},
}; import { DodiAttributeCode, DodiAttributeCodeType } from '@core/types/DodiAttributeCode';
const a = 'hello';
const b: DodiAttributeCode = a; // TS error 2322, string not valid
DodiAttributeCode.assert(a);
const c: DodiAttributeCode = a; // TS loves it.. Time to do real work now. |
Thanks for the clear explanation, this is a strange behavior from typescript. Got stumped by this error today. |
Really strange behavior const value: unknown = 0
const foo = (value: unknown): asserts value is number => { }
foo(value) // error
value // unknown
const foo_: typeof foo = foo
foo_(value) // ok
value // number const value: unknown = 0
const foo = {
bar(value: unknown): asserts value is number { }
}
foo.bar(value) // error
value // unknown
const foo_: typeof foo = foo
foo_.bar(value) // ok
value // number |
* jest is no longer needed * remove ts-check for now until microsoft/TypeScript#36931 or we migrate tests to typescript
Also chiming in that I ran into this bug in 4.9.4. I am saddened to hear that the typescript devs believe that this behavior is somehow working as intended. |
Fwiw, if const t2: typeof t = t;
t2.assertSomething(val); so that |
While I can at least understand the decision to consider this working as intended, the error message you get only makes sense after you discover the solution to the problem, which is not an ideal trait in an error message. |
I think everybody would prefer that declare function makeStruct(): {
assert(subj: unknown): asserts subj is string
guard(subj: unknown): subj is string
}
const struct = makeStruct()
declare const subj: unknown
// ts-error:
// "Assertions require every name in the call target to be
// declared with an explicit type annotation. 10:1:416 typescript2775"
struct.assert(subj)
// No such problem with `guard`
if (struct.guard(subj)) {
subj // `string` as expected
} The current restriction is highly unintuitive and reduces For example we could have import z from 'zod`
declare const subject: unknown
z.string().assert(subject)
subject // `string` |
@RyanCavanaugh is there somewhere to view the 'completed' work? |
The bulk "Close" action doesn't let you pick which "Close" you're doing. Design Limitations aren't supposed to be left open - it means we're not aware of any plausible way to address them. |
TypeScript Version:
3.9.0-dev.20200220
Search Terms:
assertion signature
Code
Expected behavior:
Assertion functions are usable as methods of a class
Actual behavior:
Assertions require every name in the call target to be declared with an explicit type annotation.(2775)
'azzert' needs an explicit type annotation.
Playground Link:
https://www.typescriptlang.org/play/?ts=3.9.0-dev.20200220&ssl=1&ssc=1&pln=33&pc=34#code/C4TwDgpgBAwg9gOwM7AE4FcDGw6oDwAqAfFALxQIQDuUAFAHSMCGqA5kgFxRMIgDaAXQCUZEgQCwAKCkAbCMCgpUXHiDJQA5ADM4cDVKlL6OAKphIqGEyQRaIgPT3FaKAEsk3Xgclb0CbK6I3Eg2qMC0mIgAJq7AgQhcfgDWCHBUCAA0UAC2ECFMrBBcSq4IrOoaAIIhEGHxUFpMrjLoqBAaWQCiqKi48MhoWDjKsIhKQ7h43b2oJOTVoXGI07hCKjVhHpEIMUsIUADeUlAnblq0AITbu-EiR5Knj1DAABa9NJQ0K5Zjg9i4tFy+UKQgA3MdTgBfKTQ6SSayLWigSBwLTOVBkcgaEplDRCbxGUzmWpWGx2KCOdFuDw41jeTAyBFQBa1PbfKAQAAewAgOw87IOUFhUgZTJZdSC90eCNZEWisXiiQQKTSmRyeSQBSK6NK5Sx4r2DSaLTaHSg33643+I0tf2GUx6uDmzI2bMdqDWwUWW3lhqlTxOrnO1wViDuEIDj1e7wo1HN7ttGGtgI1WrBEaesMesOFkjkCiYAC9C6z1J8XYt4nZ6DKwt58+iAEwqXgVABGLH0cKLJbCSPAEFRTcxmlpeIp9lqMygjYA7LOAKwEtCN4xwMwWUm2BxOJSN6meEBAA
Related Issues:
Perhaps this a design limitation per the following PR?
#33622
#33622 (comment)
The text was updated successfully, but these errors were encountered: