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?
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
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