0

I'm trying to use Conditional Mapped types to get only allow keys of an object that are of a particular type as a parameter in a function.

However, I'm running into an issue in that the correct type is not being inferred when I do so.

I've created an example to demonstrate (view on typescript playground):

interface TraversableType{
  name: string;
}

interface TypeOne extends TraversableType{
  typeNotTraversable: string;
  typeTwo: TypeTwo;
  typeThree: TypeThree;
}

interface TypeTwo extends TraversableType{
  typeTwoNotTraversable: string;
  typeOne: TypeOne;
  typeThree: TypeThree;
}

interface TypeThree extends TraversableType{
  typeThreeProp: string;
}


type TraversablePropNames<T> = { [K in keyof T]: T[K] extends TraversableType ? K : never }[keyof T];


//given start object, return 
function indexAny<T extends TraversableType, K extends keyof T>(startObj: T, key: K): T[K] {
  return startObj[key]; 
}

//same thing, but with only "traversable" keys allow
function indexTraverseOnly<T extends TraversableType, K extends TraversablePropNames<T>>(startObj: T, key: K): T[K] {
  return startObj[key]; 
}

let t2: TypeTwo;

type keyType = keyof TypeTwo;                  // "typeTwoNotTraversable" | "typeOne" | "typeThree" | "name"
type keyType2 = TraversablePropNames<TypeTwo>; // "typeOne" | "typeThree"

let r1 = indexAny(t2, 'typeOne');              // TypeOne
let r2 = indexTraverseOnly(t2, 'typeOne');     // TypeOne | TypeThree

Notice how when using K extends keyof T the indexAny function is able to infer the correct return type.

However, when I try to use the TraversablePropNames conditional mapped type to defined the key, it doesn't know if it's TypeOne or TypeTwo.

Is there some way to write the function so that it will ONLY allow keys of TraversableType AND will infer the type correctly?

UPDATE:

Interestingly... it seems to work 1 property deep IF I wrap the method in a generic class and pass the instance in (instead of as the first param). However, it only seem to work for one traversal... then it fails again:

class xyz<T>{
  private traversable: T;
  constructor(traversable: T) {
    this.traversable = traversable;
  }

   indexTraverseOnly<K extends TraversablePropNames<T>>(key: K): T[K] {
    return this.traversable[key]; 
  }

  indexTraverseTwice<K extends TraversablePropNames<T>, K2 extends TraversablePropNames<T[K]>>(key: K, key2: K2): T[K][K2] {
    return this.traversable[key][key2]; 
  }
}

let t2: TypeTwo;
let r3Obj = new xyz(t2);
let r3 = r3Obj.indexTraverseOnly('typeOne'); // TypeOne (WORKS!)

let r4 = r3Obj.indexTraverseTwice('typeOne', 'typeThree'); // TypeTwo | TypeThree

1 Answer 1

1

Because T appears in two positions for the function call (both standalone and in K) there are basically two positions that can determine the type of T. Now usually typescript can handle such cases for simple situations, but using the mapped type will cause it to give up on inferring the type of K.

There are several possible solutions, one of which you discovered, that is to fix T first. You did it with a class, you could also do it with a function that returns a function:

function indexTraverseOnly2<T extends TraversableType>(startObj: T) {
  return function <K extends TraversablePropNames<T>>(key: K): T[K] {
    return startObj[key];
  }
}

let r3 = indexTraverseOnly2(t2)('typeThree');     // TypeThree

The other solution would be to specify the constraint that K must a key in T that has a value of TraversableType in a different way, you could say that T must extend Record<K, TraversableType> meaning that key K must have the type TraversableType regardless of any other properties.

function indexTraverseOnly<T extends Record<K, TraversableType>, K extends keyof any>(startObj: T, key: K): T[K] {
  return startObj[key]; 
}

Edit

To traverse multiple types you will need to defined multiple overloads. There is unfortunately no way to do this in a single overload since the parameters are interdependent. You can define up to a reasonable number of overloads:

function indexTraverseOnly<T extends Record<K, TraversableType & Record<K2,TraversableType& Record<K3,TraversableType>>>, K extends keyof any, K2 extends keyof any, K3 extends keyof any>(startObj: T, key: K, key2:K2, key3:K3): T[K][K2][K3]
function indexTraverseOnly<T extends Record<K, TraversableType & Record<K2,TraversableType>>, K extends keyof any, K2 extends keyof any>(startObj: T, key: K, key2:K2): T[K][K2] 
function indexTraverseOnly<T extends Record<K, TraversableType>, K extends keyof any>(startObj: T, key: K): T[K]
function indexTraverseOnly(startObj: any, ...key: string[]): any {
  return null; 
}

let t2: TypeTwo;

let r1 = indexTraverseOnly(t2, 'typeOne');     // TypeOne
let r2 = indexTraverseOnly(t2, 'typeOne', 'typeTwo'); // TypeTwo
let r3 = indexTraverseOnly(t2, 'typeOne', 'typeTwo', 'typeThree'); // TypeThree
Sign up to request clarification or add additional context in comments.

9 Comments

so is there anyway to traverse multiple levels deep, while restricting the keys which can be used to a type? My use case I need to index up to five times. Also, on your second type method, is there any reason you didn't use <T extends Record<K, TraversableType>, K extends keyof T>?
@NSjonas K just need to be a key, if you defined K extends keyof T, T and K are mutually recursive and K will end up being just a key anyway .. the restriction is in the fact that T extends Record<K, TraversableType>. For multiple levels you need to define multiple overloads ... I'll add an update ..
thank you so much! I've spent sooo many hours struggling to figure this out now
so sorry to keep bothering you, I'm sure you are a busy man. But it is it possible to do this in a way that: 1: Will allow autocomplete on the key 2: Will will throw an error if the key isn't valid? For example, right now this doesn't complain at all: let r3 = indexTraverseOnly(t2, 'typeOne', 'typeTwo', 'tghjkasd'); // valid
@NSjonas or we might force an error using a conditional type.. I'll try to add a solution later
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.