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

`invariant`-style type guard #19066

Closed
sgoll opened this Issue Oct 10, 2017 · 3 comments

Comments

Projects
None yet
3 participants
@sgoll
Copy link

sgoll commented Oct 10, 2017

TypeScript Version: 2.5.3

function invariant(condition: any, message: string): void {
    if (!condition) {
        throw new Error(message);
    }
}

function double(a: number | string) {
    invariant(typeof a === 'number', 'Sorry, strings not supported yet');
    return a * 2;
}

(Playground)

Expected behavior:

invariant should establish a type guard for the code following the invariant statement.

Actual behavior:

TypeScript errors with error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.

This is because a still resolves to type string | number.

Suggestion:

invariant ensures that its first argument condition is truthy, and throws otherwise, i.e. when the code following the invariant statement is reached it is safe to make all the same assumptions as if that code were wrapped in if (condition) { … }, or equivalently, the invariant were replaced with its literal implementation.

Although the latter works as a regular type guard (if (typeof a !== 'number') { throw … }), it makes the code unnecessarily verbose. I think there should be a way of declaring that a given function guarantees that some guard is active, and never returns otherwise, i.e. something like this (pseudo code):

declare function invariant(condition: any, message: string): void if testValue;
@krryan

This comment has been minimized.

Copy link

krryan commented Oct 10, 2017

I would expect the syntactic form of any such thing to be more like this:

function invariant(condition: boolean, message: string): condition is true {
    if (!condition) {
        throw new Error(message);
    }
    return true;
}

This doesn't work as you want it to (TS doesn't seem to infer from condition is true to mean that typeof a !== 'number' and carry that inference forward), and even if it did you would need to use a condition so maybe some new syntax could be used to indicate that true is the only possible value, but something would certainly need to be in the signature of invariant to indicate that it is has this behavior.

That said, I don't see a lot of advantages to using invariant over simply saying

if (typeof a !== 'number') throw 'Sorry, strings not supported yet';

This will work exactly as you want invariant to work, without any separate utility function that you need to define. If you want to augment message somehow, you could easily define a never-returning function that does so:

function crash(message: string): never {
    throw `Something crashed: ${message}.`;
}

function double(a: number | string) {
    if (typeof a !== 'number') return crash('Sorry, strings not supported yet');
    return a * 2;
}

The project I work on uses this crash function quite liberally.

@RyanCavanaugh

This comment has been minimized.

Copy link
Member

RyanCavanaugh commented Oct 10, 2017

See #10421, #8655

@sgoll

This comment has been minimized.

Copy link
Author

sgoll commented Oct 10, 2017

@RyanCavanaugh Thanks for the reference. Though I tried looking for existing issues, I could not find any that had the invariant application that I was looking for, at least not under that name. :)

@krryan I hadn't thought about splitting the check and message formatting/post-processing etc. into a separate function. Thanks for the suggestion. I like your example of crash and will look into that.

@sgoll sgoll closed this Oct 10, 2017

@Microsoft Microsoft locked and limited conversation to collaborators Jun 14, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.