Более читаемый способ извлечения ключей типа, где типы значений для указанного ключа соответствуют интерфейсу?

В моем приложении есть куча сервисов на границе между интерфейсом и сервером, которые вызываются через 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?

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}>. `

jcalz 13.05.2019 20:09

@jcalz Только что нашел то же самое, копаясь в библиотеке служебных типов. Это определенно улучшение, поэтому, если вы ответите на него, я соглашусь.

millimoose 14.05.2019 16:49
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
2
42
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

То, что у вас есть, правильно, и единственная строго ненужная часть — это то, где вы вручную пытаетесь удалить типы 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"

Площадка ссылка на код

Хорошо, надеюсь, это поможет. Удачи!

Другие вопросы по теме