В моем приложении есть куча сервисов на границе между интерфейсом и сервером, которые вызываются через Electron IPC с использованием строковых имен. Для облегчения проверки типов и поддержки редактора у меня есть интерфейс, который сопоставляет имена сервисов с типом интерфейса:
interface IFooService {
aaa(): void;
search(query: string): any;
}
interface IBarService {
bbb(): void;
}
interface IAllServices {
foo: IFooService,
bar: IBarService,
}
Во внешнем интерфейсе есть общий компонент поиска, который может выполнять поиск в любой из служб, и я пытался избавиться от типа any свойства ввода, используемого для установки службы для вызова.
Для этого мне нужно извлечь все ключи на IAllServices, где тип значения ключа имеет метод search, в тип SearchableServiceKey. т.е. в приведенном выше примере он есть у IFooService, а у IBarService нет, поэтому я хочу, чтобы извлеченный тип был эквивалентен type SearchableServiceKey = 'foo'.
Смысл упражнения в том, что средство проверки типов не принимает это:
function search(services: IAllServices, service: string, query: string) {
services[service].search(query);
}
потому что services[service] имеет только пустое пересечение ключей IFooService и IBarService; но должен принять это:
function search(services: IAllServices, service: SearchableServiceKey, query: string) {
services[service].search(query);
}
Мне удалось заставить это работать, используя эту конструкцию:
type ServiceKey = keyof IAllServices;
interface ISearchable {
search(query: string): any;
}
type IsSearchable<SK extends ServiceKey> = IAllServices[SK] extends ISearchable
? SK
: never;
type ValuesOf<T> = T[keyof T];
type SearchableServiceKey = Exclude<
ValuesOf<{ [SK in ServiceKey]: IsSearchable<SK> }>,
never
>;
но это своего рода глоток с обходом через сопоставление ключа с его собственным типом или never для выражения предиката. Есть ли более простой способ выразить это в TypeScript?
@jcalz Только что нашел то же самое, копаясь в библиотеке служебных типов. Это определенно улучшение, поэтому, если вы ответите на него, я соглашусь.






То, что у вас есть, правильно, и единственная строго ненужная часть — это то, где вы вручную пытаетесь удалить типы never как составляющие объединения. Компилятор уже автоматически исключает never из союзов:
type Foo = never | string | never | number | never | boolean | never;
// IntelliSense shows: type Foo = string | number | boolean;
Единственное, что я хотел бы предложить, это то, что вам не нужно давать псевдонимы типов всем промежуточным шагам вашего типа, если вы не собираетесь их повторно использовать. Идея «дайте мне ключи типа объекта T, свойства которого можно присвоить типу значения V» возникает достаточно часто, поэтому я обычно определяю следующий псевдоним типа:
type KeysMatching<T, V> =
{ [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
И тогда вы можете получить тип, который вы ищете, как
type SearchableServiceKey =
KeysMatching<IAllServices, {search(q: string): any}>;
// IntelliSense shows: type SearchableServiceKey = "foo"
Хорошо, надеюсь, это поможет. Удачи!
neverиз союза исключать не надо (в союзах он уже исключен). В противном случае то, что вы сделали, в порядке, за исключением возможного наличия промежуточных типов, которые вы не будете использовать. Обычно я определяюtype KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T], и тогда у вас будетtype SearchableServiceKey = KeysMatching<IAllServices, {search(q: string): any}>. `