rid rid - 2 months ago 16
TypeScript Question

Type guard for interface fields on an else branch

interface Test<T> {
field: string | T;
}

function isString<T>(test: Test<T>): test is Test<string> {
return typeof test.field === "string";
}

function f<T>(test: Test<T>) {
if (isString(test)) {
const a = test.field; // the type of a is string
} else {
const b = test.field; // the type of b is string | T
}
}


In the above code, on the
if
branch,
a
is of type
string
, which is correct. However, on the
else
branch,
b
is of type
string | T
.

Even if I add a check for
T
, I get the same result:

function isT<T>(test: Test<T>): test is Test<T> {
return typeof test.field !== "string";
}

function f<T>(test: Test<T>) {
if (isString(test)) {
const a = test.field; // the type of a is string
} else if (isT(test)) {
const b = test.field; // the type of b is string | T
}
}


What can I do not to cast
b
to a
T
explicitly, just as I don't need to cast
a
to a
string
?

Answer Source

The problem is that the user-defined type guard is checking for the type of Test<T>, but what you want is the type of field.

Your branching looks like this:

function f<T>(test: Test<T>) {
    if (isString(test)) {
        // We have a `Test<string>`
        const a = test.field;
    } else {
        // We do not have a `Test<string>`
        const b = test.field;
    }
}

In the if branch, we have a Test<string>, and its field property is a union of string | string (which is just a string). The type looks like this:

interface Test<string> {
    field: string | string;
}

In the else branch, we have a Test<SomeNonString>, and its field property is a union of string | SomeNonString. Its type looks like this:

interface Test<SomeNonString> {
    field: string | SomeNonString;
}

In that else branch, we need to disambiguate, because field is still a union type. As long as the Test<T> interface defines field as a union type of string | T, we will need that subsequent test whenever T is not string.

Here is an example:

function isFieldString<T>(field: string | T): field is string {
    return typeof field === "string";
}

function f<T>(test: Test<T>) {
    if (isString(test)) {
        const s = test.field; // const s: string
    } else if (isFieldString(test.field)) {
        const s = test.field; // const s: string
    } else { 
        const t = test.field; // const t: T
    }
}