I am trying to create strict filtering interface:
type Filter<I, O extends I = I> = (value: I) => I extends O ? boolean : false
By writing this line I want to define a type constraint for a function which:
I
false
if value is not of expected type (I extends O
is not true
)true | false
if value is of expected type and matches filterBut Typescript ignores conditional return type:
type MessageA = { type: 'A' }
type MessageB = { type: 'B' }
type Message = MessageA | MessageB
const filter: Filter<Message, MessageA> = ({ type }) => type === 'A'
const inputMessage: Message = { type: 'B' }
if (filter(inputMessage)) {
// the following line produces error
const message: MessageA = inputMessage
// because according to TS compiler
// inputMessage is still `MessageA | MessageB`
}
Logically filter(inputMessage)
MAY produce true
if inputMessage
is of type MessageA
.
I'd like to understand "is it achievable?" and "how to write it properly?" if it is.
I am not restricted with version of typescript, currently latest (for the moment) Typescript 3.9.5 is installed. I am using VSCode 1.46, whether it makes any difference.
Solution 1
You can try this:
type Filter<T, U extends T> = (candidate: T) => candidate is U;
const filter: Filter<Message, MessageA> = (message): message is MessageA => message.type === 'A'
But you still need to explicitly define the return type (: message is MessageA
).
Solution 2
This one is more complex, but it makes your type guards (refinements) type-safe.
Create a factory for type guards like this:
namespace Refinement {
class Hit<T> {
constructor(readonly value: T) {}
}
class Miss {}
type Result<T> = Hit<T> | Miss;
export function hit<T> (value: T) {
return new Hit(value);
}
export const miss = new Miss();
export function create<T, U extends T>(refine: (candidate: T) => Result<U>): (candidate: T) => candidate is U {
return (candidate): candidate is U => refine(candidate) instanceof Hit;
}
}
Usage:
declare const inputMessage: Message;
const filter = Refinement.create(
(message: Message) => message.type === 'A'
? Refinement.hit(message)
: Refinement.miss
)
if (filter(inputMessage)) {
inputMessage; // MessageA
}
This approach is used by fp-ts
for example.