0

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!

WBT
  • 2,249
  • 3
  • 28
  • 40

2 Answers2

0

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!

Ovidijus Parsiunas
  • 2,512
  • 2
  • 8
  • 18
  • Thanks for the answer! "the resultant whatToProcess argument is an object that cannot be accessed by arbitrary strings." I'm not trying to access it by an arbitrary string though, only by a small, specified subset of strings ('attributeA' | 'attributeB'). Not included in my minimal example is that I'd also like to specify that the return type matches/is related to that of `whatToProcess` so if I pass in an A type, I can use the return value in a context where a B type would not work; typing it as that CustomType AorB doesn't preserve that. Is there another way to accomplish this? – WBT Apr 05 '22 at 19:09
  • The reason why I used the term 'arbitrary strings' is because TypeScript does not realise that those strings are actually valid keys to the ```whatToProcess``` object. Hence the use of ```keyof``` is paramount. – Ovidijus Parsiunas Apr 05 '22 at 19:35
  • To answer your question about return types, the return type can indeed be interpreted by the key that you pass into your function. Here is a link: https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgrgZ2AewLYBVzQLxQN4CGwwATgJYBGcwEAggFxRLkB2A5gL5QA++RplahABCjFnFQUIJDgG4AUPIDGyFkigQAHgVRgANhABiLKLgBmcFkuBlVAHnQbNNFgBMEsRCgxYANFABNJxd3KABrCBBkMyh0AD4ACgB3AAsidGQABRJkJQgEBEZ0f35yKhoAOR0IRgCASiKAbQCAXXx5KE6oEghgOBITVPSsnLyCxtLBSuqWhQ5FFTVgbvy4PWXcLR19IxYEvChJ8rpGAHISAjc0AGUBdlOoDn9To6FaU7qFIA – Ovidijus Parsiunas Apr 05 '22 at 19:37
0

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

Cody Duong
  • 2,292
  • 4
  • 18