У меня есть метод класса, который принимает единственный аргумент как строку и возвращает объект с соответствующим свойством type
. Этот метод используется для сужения различаемого типа объединения и гарантирует, что возвращаемый объект всегда будет иметь определенный суженный тип, который имеет предоставленное type
значение дискриминации.
Я пытаюсь предоставить сигнатуру типа для этого метода, который будет правильно сузить тип от общего параметра, но я ничего не пытаюсь сузить его от разрозненного объединения без того, чтобы пользователь явно предоставлял тип, который он должен быть сужен к. Это работает, но раздражает и чувствует себя излишне.
Надеюсь, это минимальное воспроизведение дает понять:
interface Action {
type: string;
}
interface ExampleAction extends Action {
type: 'Example';
example: true;
}
interface AnotherAction extends Action {
type: 'Another';
another: true;
}
type MyActions = ExampleAction | AnotherAction;
declare class Example<T extends Action> {
// THIS IS THE METHOD IN QUESTION
doSomething<R extends T>(key: R['type']): R;
}
const items = new Example<MyActions>();
// result is guaranteed to be an ExampleAction
// but it is not inferred as such
const result1 = items.doSomething('Example');
// ts: Property 'example' does not exist on type 'AnotherAction'
console.log(result1.example);
/**
* If the dev provides the type more explicitly it narrows it
* but I'm hoping it can be inferred instead
*/
// this works, but is not ideal
const result2 = items.doSomething<ExampleAction>('Example');
// this also works, but is not ideal
const result3: ExampleAction = items.doSomething('Example');
Я также попытался получить умный, пытаясь создать динамически настроенный тип - это довольно новая функция в TS.
declare class Example2<T extends Action> {
doSomething<R extends T['type'], TypeMap extends { [K in T['type']]: T }>(key: R): TypeMap[R];
}
Это имеет тот же результат: он не сужает тип, потому что в карте типа { [K in T['type']]: T }
значение для каждого вычисленного свойства T
не относится к каждому свойству итерации K in
, но вместо этого просто тот же союз MyActions
. Если я требую, чтобы пользователь предоставил предопределенный сопоставленный тип, который я могу использовать, это будет работать, но это не вариант, поскольку на практике это будет очень плохой опыт работы с разработчиками. (союзы огромны)
Этот пример использования может показаться странным. Я попытался перевести мою проблему в более расходную форму, но мой вариант использования действительно касается Observables. Если вы знакомы с ними, я пытаюсь более точно набрать оператор ofType
, предоставляемый сокращаемым наблюдаемым. Это в основном сокращение для filter()
в свойстве type
.
Это на самом деле супер похоже на то, как Observable#filter
и Array#filter
также сужают типы, но TS, похоже, это понимает, потому что обратные вызовы предикатов имеют возвращаемое значение value is S
. Неясно, как я мог бы приспособить что-то подобное здесь.