Typescript: получение правильного типа вывода при использовании условно сопоставленных ключей

Я пытаюсь использовать типы с условным отображением, чтобы получить только разрешающие ключи объекта определенного типа в качестве параметра функции.

Однако я сталкиваюсь с проблемой, заключающейся в том, что при этом не определяется правильный тип.

Я создал пример для демонстрации (просмотреть машинописный текст pl ayground):

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 может определить правильный тип возвращаемого значения.

Однако, когда я пытаюсь использовать TraversablePropNames тип условного сопоставления для определения ключа, он не знает, TypeOne или TypeTwo.

Есть ли способ написать функцию так, чтобы она разрешала ТОЛЬКО ключи TraversableType И правильно определяла тип?

ОБНОВИТЬ:

Интересно ... кажется, работает 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

person NSjonas    schedule 06.11.2018    source источник


Ответы (1)


Поскольку T появляется в двух позициях для вызова функции (как автономно, так и в K), в основном есть две позиции, которые могут определять тип T. Обычно машинописный текст может обрабатывать такие случаи для простых ситуаций, но использование сопоставленного типа заставит его отказаться от вывода типа K.

Есть несколько возможных решений, одно из которых вы обнаружили, а именно: сначала исправить T. Вы сделали это с помощью класса, вы также можете сделать это с помощью функции, которая возвращает функцию:

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
person Titian Cernicova-Dragomir    schedule 06.11.2018
comment
так есть ли способ пройти несколько уровней глубоко, ограничивая ключи, которые могут использоваться для типа? В моем случае мне нужно проиндексировать до пяти раз. Кроме того, в вашем методе второго типа есть ли причина, по которой вы не использовали <T extends Record<K, TraversableType>, K extends keyof T>? - person NSjonas; 06.11.2018
comment
@NSjonas K просто должен быть ключом, если вы определили K extends keyof T, T и K являются взаимно рекурсивными, и K в любом случае окажется просто ключом ... ограничение в том, что T extends Record<K, TraversableType>. Для нескольких уровней нужно определить несколько перегрузок ... Я добавлю обновление .. - person Titian Cernicova-Dragomir; 06.11.2018
comment
большое спасибо! Я потратил ооочень много часов, пытаясь понять это сейчас - person NSjonas; 06.11.2018
comment
извините, что беспокою вас, я уверен, что вы занятой человек. Но можно ли сделать это таким образом, чтобы: 1: разрешить автозаполнение для ключа 2: вызвать ошибку, если ключ недействителен? Например, прямо сейчас на это вообще не жалуется: let r3 = indexTraverseOnly(t2, 'typeOne', 'typeTwo', 'tghjkasd'); // valid - person NSjonas; 06.11.2018
comment
просто чтобы прояснить, я пишу библиотеку генератора запросов для языка SOQL-esk, поэтому получение типизации в параметрах на самом деле является самым важным. - person NSjonas; 06.11.2018
comment
@NSjonas Я думаю, что это будет работать с функцией, которая возвращает функциональный подход .. Мне нужно протестировать, и я просто работаю .. - person Titian Cernicova-Dragomir; 06.11.2018
comment
не стоит беспокоиться! Спасибо за помощь! Я попытаюсь понять это, используя этот подход (может снова вас сбить, если я действительно застряну) - person NSjonas; 06.11.2018
comment
@NSjonas, иначе мы можем вызвать ошибку, используя условный тип. Я постараюсь добавить решение позже - person Titian Cernicova-Dragomir; 06.11.2018
comment
Я заставил его работать, используя подход функции карри. Мне нужно будет настроить API моих библиотек, но в целом это не проблема. Единственное, в чем я не уверен, - это как реализовать различную глубину отношений. В идеале его можно было бы перегрузить, чтобы можно было вызвать одну функцию, вместо того, чтобы вызывать конкретную функцию (EG indexTraverse3()), которая проходит необходимое вам количество раз. - person NSjonas; 06.11.2018