打字稿:使用条件映射键时获取正确的推理类型

Typescript: Getting correct inference type when using conditional mapped keys

我正在尝试使用 Conditional Mapped types 来仅允许特定类型的对象键作为函数中的参数。

但是,我 运行 遇到了一个问题,因为当我这样做时没有推断出正确的类型。

我创建了一个示例来演示 (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

注意在使用 K extends keyof T 时,indexAny 函数如何能够推断出正确的 return 类型。

但是,当我尝试使用 TraversablePropNames 条件映射类型来定义键时,它不知道它是 TypeOne 还是 TypeTwo

有没有什么方法可以编写函数,使其只允许 TraversableType 的键并正确推断类型?

更新:

有趣的是...如果我将方法包装在通用 class 中并将实例传入(而不是作为第一个参数),它似乎工作 1 属性 深度。然而,它似乎只适用于一次遍历......然后它再次失败:

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

因为T出现在函数调用的两个位置(独立的和K中)所以基本上有两个位置可以确定T的类型。现在通常 typescript 可以处理简单情况下的这种情况,但是使用映射类型会导致它放弃推断 K 的类型。

有几种可能的解决方案,其中一种是您发现的,那就是先修复 T。你用 class 做的,你也可以用 returns 一个函数做的:

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

另一种解决方案是指定 K 必须以不同方式在 T 中具有值 TraversableType 的键的约束,您可以说 T 必须扩展 Record<K, TraversableType> 意味着键 K 必须具有类型 TraversableType 而不管任何其他属性。

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

编辑

要遍历多种类型,您需要定义多个重载。不幸的是,由于参数是相互依赖的,因此无法在单个重载中执行此操作。您最多可以定义合理数量的重载:

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