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:
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;
// ^?
}