How do I access a property of a conditional type with generics?

859 views Asked by At

The following code fails as TypeScript:

const exampleFn = function<AttributeName extends 'attributeA' | 'attributeB'>(
    whatToProcess: AttributeName extends 'attributeA' ? {attributeA: string} : {attributeB: string},
    attributeName: AttributeName
) {
    //Error here: Type 'AttributeName' cannot be used to index type 
    //'AttributeName extends "attributeA" ? { attributeA: string; } : { attributeB: string; }'.ts(2536)
    console.log(whatToProcess[attributeName]);
}

Playground link here.

When attributeName is 'attributeA', whatToProcess should have attributeA, and when attributeName is 'attributeB', whatToProcess should have attributeB, so in each case attributeName should be usable to index the type of whatToProcess.

It seems I'm not understanding something about how the generic + conditional typing system works in TypeScript; if someone can help me figure out how this is supposed to be done that'd be much appreciated!

2

There are 2 answers

3
Ovidijus Parsiunas On

TypeScript cannot link AttributeName as a key of {attributeA: string} or {attributeB: string} because the resultant whatToProcess argument is an object that cannot be accessed by arbitrary strings. Additionally, TypeScript will not know what the object is going to be in runtime, hence you will be forced to build code to cater for all cases either way.

I believe there is a better way to solve your problem. What you are trying to constrain here is the ability for the developer to access the object using specific keys. This object could be either {attributeA: string} or {attributeB: string} which is perfect for a unary type - CustomType, and all you need to do is to make sure that the second parameter is a key of the unary type: keyof CustomType as follows:

type CustomType = {attributeA: string} | {attributeB: string};

const exampleFn = function(whatToProcess: CustomType, attributeName: keyof CustomType) {
    console.log(whatToProcess[attributeName]);
}

Link to the TypeScript Playground.

Hopefully this is helpful!

0
Cody Duong On

You can use a generic to allow TS to use inferencing,

const exampleFn = function<T extends Record<any, any>>
    (whatToProcess: T, attributeName: keyof T) 
{
    console.log(whatToProcess[attributeName]);
}

You can either accept any object, or restrict it to a subset of objects like so

type CustomType = {attributeA: string} | {attributeB: string};

const exampleFn = function<T extends CustomType>
    (whatToProcess: T, attributeName: keyof T) 
{
    console.log(whatToProcess[attributeName]);
}

And likewise you can rely on that to make a custom return type

const exampleFnBlowUpOnAttributeA = function<T extends CustomType>
    (whatToProcess: T, attributeName: keyof T): T extends {attributeA: string} ? never : string
{
    console.log(whatToProcess[attributeName]);
    return null! //fake implementation
}

View this on TS Playground