Home > Enterprise >  Typescript type guard that only partially narrows
Typescript type guard that only partially narrows

Time:11-13

I am trying to make a type guard that narrows the string type but doesn't narrow it all the way to an explicit list of strings.

Here is an example of the type of code I am writing:


type Node = { type: string, content: string };
type Macro extends Node { type: "macro" };

function generateGuard(filter: Set<string>) {
    function strGuard(s: any): s is Macro & {content: unknown} {
        return typeof s === "object" &&
                  s.type === "macro" && 
                  filter.has(s.content);
    }
    return strGuard;
}
const specialMacro = generateGuard(new Set(["x"]));

let xxx: Macro = {type: "macro", content: "some string" };
if (specialMacro(xxx)) {
    // Type Macro
    xxx;
} else {
    // Type never
    xxx;
}

The issue is that xxx has type never in the else block. I want it to have some sort of T extends Macro type instead (or a the full Macro type would be fine, it just cannot be never). The & {content: unknown} doesn't narrow the type because {content: string} is already more specific.

Is there some way to type generateGuard such that it narrows {content: string} by some "unknown" amount?

CodePudding user response:

The issue is that you're hiding several interesting bits from TypeScript. By specifying that the set is a set of distinct string types you can get the narrowing you want without introducing a too-specific constraint in content in the false case:

type AstNode = { type: string, content: string };
type Macro = AstNode & { type: "macro" };

function generateGuard<C extends string>(filter: Set<C>) {
    function strGuard(s: any): s is Macro & {content: C} {
        return typeof s === "object" &&
                  s.type === "macro" && 
                  filter.has(s.content);
    }
    return strGuard;
}

Usage now works:

const specialMacro = generateGuard(new Set(["x", "y", "z"]));

let oneNode: AstNode = {type: "macro", content: "some string" };
if (specialMacro(oneNode)) {
    // `oneNode` is type Macro
    oneNode.content // and content is of type "x" | "y" | "z";
} else {
    // Type AstNode
    oneNode.content;  // and content is of type `string`
}

See it in action on the playground

CodePudding user response:

Playground Link

function hasPrefix<P extends string>(str: string, prefix: P): str is `${P}${string}` {
    return typeof str === 'string' && str.startsWith(prefix);
}
declare const Brand: unique symbol;
type Id = string & {[Brand]?: 'id'}
function isId(str: string): str is Id {
    return !!str.match(/^\w $/);
}

let xxxx = "some string" as const;
if (hasPrefix(xxxx, 'some')) {
    xxxx;
    // ^?
} else {
    xxxx;
    // ^?
}
if (hasPrefix(xxxx, 'soon')) {
    xxxx;
    // ^?
} else {
    xxxx;
    // ^?
}
let xxxy = "some string";
if (hasPrefix(xxxy, 'wasd')) {
    xxxy;
    // ^?
} else {
    xxxy;
    // ^?
}
if (isId(xxxy)) {
    xxxy;
    // ^?
} else {
    xxxy;
    // ^?
}
  • Related