5

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?

Rick
  • 8,366
  • 8
  • 47
  • 76

3 Answers3

4

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

Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • This will work, but the problem is if `keyof McuParams` changes, the type guard may be incorrect. It also requires duplication, as I will need to keep a list of strings that are used at compile time and another list that will be used at runtime. Perhaps I'm going beyond the scope of TS by trying to get compile time information at runtime? – Rick Feb 01 '17 at 18:25
  • 1
    You can implement `isKeyOfMcuParams` however you want, e.g. if there's a dynamic way to fetch it, use that. If the information about which keys are valid isn't manifest yet, well, there's no way around that. – Ryan Cavanaugh Feb 01 '17 at 18:35
  • Could you checkout https://stackoverflow.com/questions/55850174/typescript-user-defined-type-guards-for-literal-types – SumNeuron Apr 25 '19 at 13:15
1

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);        
     }
});
FRocha
  • 942
  • 7
  • 11
0

Updated:

Based on my updated understanding and the background info you provided, is this the snippet that relates to your use case? If so, it compiles fine for me.

interface McuParams {
    foo: string;
    bar: string;
};

function change(key: keyof McuParams, dispatch: Function) {
    if (typeof key === 'foo') {
        console.log('call foo()');
    } else if (typeof key === 'bar') {
        console.log('call bar()');
    }
}

function callback(data) {
    change(data, this.props.dispatch)
}
benjaminz
  • 3,118
  • 3
  • 35
  • 47
  • This is not quite what I was asking for. The above example works with string literals like `'foobar'` (and could be further simplified with the `keyof` operator), but it will not work with data coming from an API which is not known at compile time. – Rick Feb 01 '17 at 18:27
  • @Rick are you talking about my example? `'foobar'` will not compile, but it could work, as you said, at runtime. May ask what type is: `McuParams`? – benjaminz Feb 01 '17 at 19:06
  • It's a standard interface (Object). The `keyof` operator converted it to `"a" | "string" | "type"` – Rick Feb 01 '17 at 19:50