Как в TypeScript получить ключи типа объекта, значения которого относятся к данному типу?

Я пытался создать тип, состоящий из ключей типа T, значения которых являются строками. В псевдокоде это будет keyof T where T[P] is a string.

Единственный способ, которым я могу это сделать, - это два шага:

// a mapped type that filters out properties that aren't strings via a conditional type
type StringValueKeys<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };

// all keys of the above type
type Key<T> = keyof StringValueKeys<T>;

Однако компилятор TS говорит, что Key<T> просто равно keyof T, хотя я отфильтровал ключи, значения которых не являются строками, установив для них never с использованием условного типа.

Таким образом, он все еще позволяет это, например:

interface Thing {
    id: string;
    price: number;
    other: { stuff: boolean };
}

const key: Key<Thing> = 'other';

когда единственное допустимое значение key действительно должно быть "id", а не "id" | "price" | "other", поскольку значения двух других ключей не являются строками.

Ссылка на образец кода на игровой площадке TypeScript


person Aron    schedule 04.02.2019    source источник
comment
Возможно дублирование определения общей функции сортировки машинописного текста определенного типа или, по крайней мере, мой ответ такой же   -  person jcalz    schedule 04.02.2019


Ответы (2)


Это можно сделать с помощью условных типов и типы поиска, вот так:

type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];

а затем вы извлекаете ключи, свойства которых совпадают string следующим образом:

const key: KeysMatching<Thing, string> = 'other'; // ERROR!
// '"other"' is not assignable to type '"id"'

В деталях:

KeysMatching<Thing, string> ➡

{[K in keyof Thing]-?: Thing[K] extends string ? K : never}[keyof Thing] ➡

{ 
  id: string extends string ? 'id' : never; 
  price: number extends string ? 'number' : never;
  other: { stuff: boolean } extends string ? 'other' : never;
}['id'|'price'|'other'] ➡

{ id: 'id', price: never, other: never }['id' | 'price' | 'other'] ➡

'id' | never | never ➡

'id'

Обратите внимание, что вы делали:

type SetNonStringToNever<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };

на самом деле просто превращал значения нестроковых свойств в never значения свойств. Это не касалось клавиш. Ваш Thing станет {id: string, price: never, other: never}. И ключи у него такие же, как у Thing. Основное различие между этим и KeysMatching заключается в том, что вы должны выбирать ключи, а не значения (то есть P, а не T[P]).

Надеюсь, это поможет. Удачи!

person jcalz    schedule 04.02.2019
comment
Кажется, это работает! Не могли бы вы вкратце объяснить, почему это работает, а моя попытка - нет? - person Aron; 04.02.2019
comment
Хорошо, это имеет смысл, хотя не уверен, что сам когда-либо придумал это. Спасибо @jcalz! - person Aron; 04.02.2019
comment
Это прекрасно работает! Не могли бы вы объяснить, как это работает? Спасибо! - person Akash; 06.07.2021
comment
Не могли бы вы подробно объяснить, как работает этот код? Я недавно начал использовать машинописный текст, и я знаю основы, но не сложные комбинации подобных вещей. Спасибо! - person Akash; 06.07.2021

В качестве дополнительного ответа:

Начиная с версии 4.1 вы можете использовать переназначение клавиш для альтернативного решения (обратите внимание, что основная логика не отличается от ответа jcalz). Просто отфильтруйте ключи, которые при использовании для индексации исходного типа не создают тип, присваиваемый целевому типу, и извлеките объединение оставшихся ключей с keyof:

type KeysWithValsOfType<T,V> = keyof { [ P in keyof T as T[P] extends V ? P : never ] : P };

interface Thing {
    id: string;
    price: number;
    test: number;
    other: { stuff: boolean };
}

type keys1 = KeysWithValsOfType<Thing, string>; //id -> ok
type keys2 = KeysWithValsOfType<Thing, number>; //price|test -> ok

площадка


Как справедливо заметил Михал Миних:

Оба могут извлекать объединение строковых ключей. Тем не менее, когда их следует использовать в более сложной ситуации - например, T extends Keys ... ‹T, X›, тогда TS не сможет хорошо понять ваше решение.

Поскольку приведенный выше тип не индексируется с keyof T, а вместо этого использует keyof сопоставленного типа, компилятор не может сделать вывод, что T индексируется выходным объединением. Чтобы гарантировать компилятору это, можно пересечь последнее с помощью keyof T:

type KeysWithValsOfType<T,V> = keyof { [ P in keyof T as T[P] extends V ? P : never ] : P } & keyof T;

function getNumValueC<T, K extends KeysWithValsOfType<T, number>>(thing: T, key: K) {
    return thing[key]; //OK
}

Обновленная площадка

person Oleg Valter    schedule 10.02.2021
comment
Хотя этот ответ и ответ от jcalz генерируют один и тот же тип, вариант от jcalz лучше, потому что Typescripts запоминает, что результирующие ключи поступают из исходного объекта T и могут использоваться позже для его индексации, как в T [K]. с этим ответом TS 4.3 не знает этого и выдает ошибку Тип "K" не может использоваться для индексации типа "T" .ts (2536) - person Michal Minich; 10.03.2021
comment
В ТС 4.3? Я думал, что у нас почти не будет 4.2.3 (вероятно, просто опечатка) Re: indexing - я что-то упустил (tsplay.dev/ m3Aabw), не могли бы вы добавить ссылку на детскую площадку, чтобы посмотреть? - person Oleg Valter; 10.03.2021
comment
См. tsplay.dev/mxozbN. Здесь показано использование обоих решений (от jcalz и вашего). Оба могут извлекать объединение строковых ключей. Тем не менее, когда их следует использовать в более сложной ситуации - например, T extends Keys...<T, X>, тогда TS не сможет хорошо понять ваше решение. Может быть, это ошибка, о которой стоит сообщить. Я заметил, что решение jcalz даже лучше в ночное время, когда оно понимает тип свойства вычисленных ключей, используемых для объекта и. - person Michal Minich; 19.04.2021
comment
@MichalMinich - спасибо, я думал, ты никогда не вернешься к этому :) Сегодня мы рассмотрим поближе, но похоже, что индексирование с keyof T позволяет компилятору узнать, что результирующий тип keyof T в моей версии, из-за использования keyof сопоставленного типа, компилятор этого не знает. Не похоже на ошибку, я понимаю логику этого. Я вижу два выхода из этого. Первый - пересечься с keyof T, чтобы убедить компилятор. Второй - Extract<[our type], keyof T>. Оба работают, поправлю ответ - person Oleg Valter; 19.04.2021
comment
^ но последнее, очевидно, более подробное или практически бесполезное, поэтому я бы остановился на пересечении keyof T - он намного элегантнее. Но это, наверное, слишком много keyofs :) - person Oleg Valter; 19.04.2021