Typescript type guard not inferred string literal

113 Views Asked by At

I try to understand is such behavior a bug?
I defined guard(a: T|undefined) and passed to it value of type "admin"|undefined which is obviously subtype of string|undefined. But guard did not removed |undefined after filtering.

It is tricky for me, because if I change if (user === 'Jack') return 'admin' to if (user === 'Jack') return 'admin' as string; then type guard works correctly, string|undefined becomes string.


function getPermissions(user: string){
    if (user === 'Jack') return 'admin';
    return undefined;
}
const admins = ['Mike', 'Joe'].map(e => getPermissions(e)); //type ("admin"|undefined)[]

function isDefined<T>(a: T | undefined): a is T {
    return a !== undefined;
}

const foundAdmins = admins.filter(isDefined); //type is ("admin"|undefined)[] but string[] expected

You could run it in a playground typescrypt playground

1

There are 1 best solutions below

0
On

The problem is with generic widenind "admin" to string

You can make that work the same way, buuuuuut with typescript@^5

Playground

function getPermissions(user: string){
    if (user === 'Jack') return 'admin';
    return undefined;
}
const admins = ['Mike', 'Joe'].map(e => getPermissions(e)); //type ("admin"|undefined)[]

function isDefined<T>(a: T | undefined): a is T {
    return a !== undefined;
}

const foundAdmins = admins.filter(isDefined); 
//    ^?
// const foundAdmins: ("admin" | undefined)[]

function isDefinedWithConst<const T>(a: T | undefined): a is T {
    return a !== undefined;
}

const foundAdmins2 = admins.filter(isDefinedWithConst); 
//    ^?
// const foundAdmins2: "admin"[]
// dirty but works
const foundAdmins3 = admins.filter((e): e is 'admin' => isDefined<'admin'>(e)); 
//    ^?
// const foundAdmins3: "admin"[]

or make an overload for that specific case

function nonNullable<T>(v: T | null | undefined): v is T { return v != null }
interface Array<T> {
    filter(p: typeof nonNullable): NonNullable<T>[]
}
const foundAdmins4 = admins.filter(nonNullable);
//    ^?
// const foundAdmins4: "admin"[]