Skip to content

invariant-style type guard #19066

Closed
Closed
@sgoll

Description

@sgoll

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;

Activity

krryan

krryan commented on Oct 10, 2017

@krryan

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

RyanCavanaugh commented on Oct 10, 2017

@RyanCavanaugh
Member
sgoll

sgoll commented on Oct 10, 2017

@sgoll
Author

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

locked and limited conversation to collaborators on Jun 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @sgoll@krryan@RyanCavanaugh

        Issue actions

          `invariant`-style type guard · Issue #19066 · microsoft/TypeScript