Home > OS >  typescript param type dependency from value not working
typescript param type dependency from value not working

Time:11-27

interface AddDataRequest{
    data:any
}
interface AddDataResponse{
    id:string
}
interface ITest{
    addData(json:AddDataRequest):Promise<AddDataResponse>
    removeData(json:AddDataResponse):Promise<boolean>
}
function testInterface<A extends keyof ITest>(action:A,req:Parameters<ITest[A]>[0],res:Awaited<ReturnType<ITest[A]>>){
    if(action === 'addData'){
        console.log(res.id); //TS2339: Property 'id' does not exist on type 'boolean | AddDateResponse'. Property 'id' does not exist on type 'false'.
    }
    else if(action==='removeData'){
        console.log(req.id); //TS2339: Property 'id' does not exist on type 'AddDateRequest | AddDateResponse'. Property 'id' does not exist on type 'AddDateRequest'.
    }
}
testInterface('removeData', {id:'test123'},true);//But works here

I am trying to make type dependency of function parameters from first one. It works when I'm trying to call the function but not works inside of it. Errors are in comments inside the code.

CodePudding user response:

This is a common difficulty: maintaining the correlation between function parameters types, when type narrowing one of them.

The usual workaround is to gather these correlated parameters into a single object, and typing the latter as a discriminated union of the different possibilities (e.g. by using a mapped type and indexed access type).

That way, when we perform type narrowing on the discriminant property (here action), TypeScript is able to narrow the entire object:

// Build a discriminated union:
type TTestFullUnion = {
    //^? { action: "addData"; req: AddDataRequest; res: AddDataResponse } | { action: "removeData"; req: AddDataResponse; res: boolean }
    [kind in keyof ITest]: { // Mapped type to describe the correlation
        action: kind, // Discriminant property
        req: Parameters<ITest[kind]>[0],
        res: Awaited<ReturnType<ITest[kind]>>
    }
}[keyof ITest] // Indexed access type to produce a union out of the mapped type

function testInterface2<ActionFull extends TTestFullUnion>(actionFull: ActionFull) {
    const { action, req, res } = actionFull;
    if (action === 'addData') {
        console.log(res.id); // Okay
        //          ^? AddDataResponse
    }
    else if (action === 'removeData') {
        console.log(req.id); // Okay
        //          ^? AddDataResponse
    }
}
testInterface2({
    action: 'removeData',
    req: { id: 'test123' },
    res: true
}); // Okay

Unfortunately, that means slightly changing your API.

Playground Link

  • Related