7

Note: I'm new to typescript. Prior to posting I read the docs regarding advance types and type guards. In addition I also read several related S.O. question (e.g. user defined type guards [typescript], and How to write a user defined type guard for "string" | "literal" | "types"?)

Most similar to my question is the later question where you might have some custom type on a literal (in this instance string, but the solution should apply to number as well) e.g.

type Format = 'JSON' | 'CSV' | 'XML'

In the second question the user asks in regards to a solution of typescript's keyof key word and @Ryan Cavanaugh's answer goes about this via changing the type from a literal to an interface and checking the keys of the interface:

// copy-pasted from answer for convenience
interface McuParams {
    foo, bar, baz;
}
function isKeyOfMcuParams(x: string): x is keyof McuParams {
    switch (x) {
        case 'foo':
        case 'bar':
        case 'baz':
            return true;
        default:
            return false;
    }
}

My question is specifically if there is a way to do user defined type guards using the type itself e.g.

const isFormat = (maybe:String|Format): maybe is Format => /* something goes here */

To my knowledge the following does not work (replacing just /* something goes here */):

// 1
/* 
 * As stated in the docs "The right side of the instanceof needs to 
 * be a constructor function" but we have a literal
 */
maybe instaceof Format

//2
/* As stated in the docs "typename" must be "number", 
 * "string", "boolean", or "symbol" 
 */
typeof maybe === 'format'


//3
/* no idea */
(<Format>maybe)

So is @Ryan Cavanaugh's answer the only viable solution? It seems extremely verbose...

SumNeuron
  • 4,850
  • 5
  • 39
  • 107
  • 1
    Bear in mind that, with the exception of enum, TypeScript is entirely stripped from the runtime JS actually evaluated. So `instanceof` and the like will only ever work with actual class instances, not abstract TypeScript types. Ultimately type guards can only work when they operate on real JS values, not pure types ... I think. – Jed Richards Apr 25 '19 at 13:28

1 Answers1

19

The best way to do this is to derive the type Format from a value like an array which contains all of the Format literals. There are a number of ways to do this. I will show the easiest way assuming you are using TS3.4+:

const formats = ['JSON', 'CSV', 'XML'] as const;
type Format = typeof formats[number];

You can verify that Format is the same as before. Now, armed with an array, you can use it to build a type guard:

function isFormat(x: string): x is Format {
    // widen formats to string[] so indexOf(x) works
    return (formats as readonly string[]).indexOf(x) >= 0;
}

That should work as expected and not be too verbose (or at least it doesn't repeat string literals anywhere). Hope that helps; good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • this no longer seems to work? `Enum declarations can only merge with namespace or other enum declarations.` – SumNeuron May 07 '20 at 09:25
  • Where are you getting such an error? This answer doesn’t have to do with enums or namespace merging so I am very confused. – jcalz May 07 '20 at 11:40