Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a user defined type guard for "string" | "literal" | "types"?

Tags:

typescript

I have the following function:

change(key: keyof McuParams, dispatch: Function) {
  /** Body removed for brevity */
}

When I call the function...

this.change(VARIABLE_FROM_MY_API, this.props.dispatch)

... I (understandably) get the following error:

Argument of type 'string' is not assignable to parameter of type '"foo" | "bar"'

This makes sense since there is no way for the compiler to know what my API is sending at compile time. However, user defined type guards can sometimes be used to infer type information at runtime and pass that information to the compiler via conditionals.

Is it possible to write a user defined type guard for a keyof string type such as keyOf foo when foo is defined ONLY as a type (and not in an array)? If so, how?

like image 461
Rick Avatar asked Feb 01 '17 16:02

Rick


2 Answers

Here's an example:

interface McuParams {
    foo, bar, baz;
}

function change(key: keyof McuParams, dispatch: Function) {
}

function isKeyOfMcuParams(x: string): x is keyof McuParams {
    switch (x) {
        case 'foo':
        case 'bar':
        case 'baz':
            return true;
        default:
            return false;
    }
}

function doSomething() {
    const VAR_FROM_API = <string>'qua';
    if (!isKeyOfMcuParams(VAR_FROM_API)) return;
    change(VAR_FROM_API, () => { });
}

In doSomething, you can use whatever control flow block you like instead of return (e.g. an if, or throw, etc).

like image 183
Ryan Cavanaugh Avatar answered Oct 18 '22 00:10

Ryan Cavanaugh


Try the following:

 enum mcuParams { foo, bar };
 type McuParams = keyof typeof mcuParams;    

 function isMcuParams(value: string) : value is McuParams {        
     return mcuParams.hasOwnProperty(value);        
 }

 function change(key: McuParams) {
     //do something
 }

 let someString = 'something';

if (isMcuParams(someString)) {
    change(someString);
 }

UPDATED:

The example I wrote above assumed we already knew the possible values of McuParams ('foo' or 'bar'). The example below do not make any assumptions. I tested it and it worked as expected. Each time you run the code you get a different response depending on the values randomly generated.

function getAllowedKeys() {
    //get keys from somewhere. here, I generated 2 random strings just for the sake of simplicity
    let randomString1 = String(Math.round(Math.random())); //0 or 1
    let randomString2 = String(Math.round(Math.random())); //0 or 1
    return Promise.resolve([randomString1, randomString2]);
}

function getKeyToBeTested() {   
    //same as in 'getAllowedKeys' 
    let randomString = String(Math.round(Math.random())); //0 or 1
    return Promise.resolve(randomString);        
}

Promise.all([getAllowedKeys(), getKeyToBeTested()]).then((results) => {    
    let allowedKeys: string[] = results[0];
    let keyTobeTested: string = results[1]; //'0' or '1'
    let mcuParams = {};

    for (let entry of results[0]) { //create dictionary dynamically     
        mcuParams[entry] = ''; //the value assigned is indiferent        
    }

    //create type dynamically. in this example, it could be '0', '1' or '0' | '1'
    type McuParams = keyof typeof mcuParams; 

    //create Type Guard based on allowedKeys fetched from somewhere
    function isMcuParams(value:string) : value is McuParams { 
        return mcuParams.hasOwnProperty(value);
    }

    function change(key: McuParams) { 
        //do something
        alert('change function executed: [' + allowedKeys.toString() + '] - ' + keyTobeTested);                
    }             

    if (isMcuParams(keyTobeTested)) {
        change(keyTobeTested);
    }
    else {
        alert('change function not executed: [' + allowedKeys.toString() + '] - ' + keyTobeTested);        
     }
});
like image 1
FRocha Avatar answered Oct 18 '22 00:10

FRocha