Typescript: Assertion functions don't work when defined as methods

Created on 21 Feb 2020  Â·  13Comments  Â·  Source: microsoft/TypeScript

TypeScript Version:

3.9.0-dev.20200220

Search Terms:

assertion signature

Code

type Constructor<T> = new (...args: any[]) => T

let str: any = 'foo'

str.toUpperCase() // str is any

function assert(condition: unknown, message: string = 'Assertion failure', ErrorConstructor: Constructor<Error> = AssertionError): asserts condition {
    if(!condition) {
        throw new ErrorConstructor(message);
    }
}

assert(typeof str == 'string')

str.toUpperCase() // str is string

class AssertionError extends Error { }

class Assertion {
    assert(condition: unknown, message: string = 'Assertion failure', ErrorConstructor: Constructor<Error> = AssertionError): asserts condition {
        if(condition) {
            throw new ErrorConstructor(message);
        }
    }
}

let azzert = new Assertion().assert

let str2: any = 'bar'

azzert(typeof str2 == 'string')  //error 2775

str2.toUpperCase() // str2 is any

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?

https://github.com/microsoft/TypeScript/pull/33622
https://github.com/microsoft/TypeScript/pull/33622#issuecomment-575301357

Design Limitation

Most helpful comment

@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:

export function exists(value: unknown): asserts value {
  assert(value);
}

But when you used it, you imported it like this:

import assert from "./assert";

assert.exists(true); // error ts(2775)

The 2775 error you are getting on that line is not about exists needing an explicit type, it's about assert needing one. You can verify this by importing exists directly:

import { exists } from "./assert";
exists( true ); // works without the error.

So the error isn't telling you about problems with exists, it's telling you that your default export needs to be explicitly typed:

const defaults: {
  exists( value: unknown ): asserts value;
} = {
  exists,
};

export default defaults;

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:

export * from './real-assertions';
import * as assertions from './real-assertions';
export default assertions;

I'm also not entirely clear on why this works, but it seems like it does..

All 13 comments

Assertion functions always need explicit annotations. You can write this:

let azzert: Assertion['assert'] = new Assertion().assert;

@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 import { exists } from "./assert" then it works but I'd like to have them grouped on the object assert.xyz.

I am also seeing error ts(2775) when using require instead of import

Following has error:

const assert = require('assert')

Following has no error

import assert from 'assert'

Troubleshoot Info

> tsc --version
Version 3.8.3

@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:

Assertion functions always need explicit annotations

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?

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 value: unknown is the issue? It needs to be non-generic and not something such as any or unknown? Is that what you mean?

@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:

export function exists(value: unknown): asserts value {
  assert(value);
}

But when you used it, you imported it like this:

import assert from "./assert";

assert.exists(true); // error ts(2775)

The 2775 error you are getting on that line is not about exists needing an explicit type, it's about assert needing one. You can verify this by importing exists directly:

import { exists } from "./assert";
exists( true ); // works without the error.

So the error isn't telling you about problems with exists, it's telling you that your default export needs to be explicitly typed:

const defaults: {
  exists( value: unknown ): asserts value;
} = {
  exists,
};

export default defaults;

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:

export * from './real-assertions';
import * as assertions from './real-assertions';
export default assertions;

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 Ember namespace. So we have:

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 Ember namespace, with a re-export like this:

import * as EmberDebugNs from '@ember/debug';

export namespace Ember {
    // ...
    const assert: typeof EmberDebugNs.assert;
    // ...
}

```ts
const { assert } = Ember;
assert('this FAILS', typeof 'hello' === 'string');

> 'assert' needs an explicit type annotation.

I understand the constraints as specified, and I'm afraid I must disagree with [the team's conclusion about this error message](https://github.com/microsoft/TypeScript/pull/33622#discussion_r330195892)—

> We discussed in the team room and agreed that the existing message is fine, so I didn't change it.

—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:

```ts
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 (typeof Http) notation because typescript complained that namespaces can't be used as types.)

Anyway, just wanted to add this for anyone who may be struggling with something similar. Thanks again for the tip, @RyanCavanaugh!

For what it's worth this has made the Assert library usable again when checking JavaScript with Typescript.

const Assert = /** @type {typeof import('assert')} */(require('assert'))

All the type errors reported about non-explicit types are gone (thankfully).

Was this page helpful?
0 / 5 - 0 ratings