Home > OS >  How to narrow object type based on field presence?
How to narrow object type based on field presence?

Time:06-22

I have 2 similar endpoints, that's one for getting a list, the other for getting a filtered list. Now it's not possible to merge them into one endpoint with query parameters on backend. So I have a function that decides which endpoint to use based on presence of id field. The problem is that I cannot get correct type inference, args couldn't be narrowed.

Playground link

How to do it? Maybe, it's better to use union, I see no discriminant field, though?

export interface Arguments {
    id?: number;
    token?: string;
    pageSize: number;
}
    
function requestTo1stEndpoint(a: {
    id: number;
    token?: string;
    pageSize: number;
}) {
    return Promise.resolve(1);
}

function requestTo2ndEndpoint(a: {
    token?: string;
    pageSize: number;
}) {
    return Promise.resolve(2);
}
    
async function request(args: Arguments) {
    let fun: typeof requestTo1stEndpoint | typeof requestTo2ndEndpoint
    if (args.id !== undefined) {
        fun = requestTo1stEndpoint
    } else
        fun = requestTo2ndEndpoint
        
    const response = await fun(args)
}

This doesn't work either:

async function request(args: Arguments) {
    let responsePromise: Response

    // this doesn't work either
    // if ('id' in args) 
    if (args.id !== undefined) {
        // cannot be narrowed
        responsePromise = requestTo1stEndpoint(args)
    } else
        responsePromise = requestTo2ndEndpoint(args)

    const response = await responsePromise
}

CodePudding user response:

You can do this with a User Defined Type Guard and "in" Narrowing.

To make this work, we also need to have the code that depends on the type of args inside the true or false of the if that uses the type guard. This is because the compiler "knows" it is Type1 if the true portion executes and it is not Type1 in the else.

interface Type1{
    id: number;
    token?: string;
    pageSize: number;
}

interface Type2 {
    token?: string;
    pageSize: number;
}

function isType1(x: Type1 | Type2): x is Type1 {
    if ('id' in x) return true;
    return false;
}

function requestTo1stEndpoint(a: Type1) {
    return Promise.resolve(1);
}

function requestTo2ndEndpoint(a: Type2) {
    return Promise.resolve(2);
}

async function request(args: Type1 | Type2) {
    const response = isType1(args) ? await requestTo1stEndpoint(args) : await requestTo2ndEndpoint(args);
}

TypeScript Playground

  • Related