Let's say I have three types (and a union one)
type A = {
type: 'a'
title: string
description: string
}
type B = {
type: 'b'
title: string
}
type C = {
type: 'c'
description: string
}
type D = A | B | C
I know I can get a correct type inference with the === operator
function logger(t: D) {
if (t.type === 'a' || t.type === 'b') console.log(t.title) // no problems here
if (t.type === 'a' || t.type === 'c') console.log(t.description) // no problems here
}
However, would it be possible to write an utility function:
function matches<T extends { type: string }>(t: T, types: T['type'][]) : boolean {
return types.includes(t.type)
}
So I can do this without errors?
function logger(t: D) {
if (matches(t, ['a', 'b'])) console.log(t.title) // Property 'title' does not exist on type D
if (matches(t, ['a', 'c'])) console.log(t.description) // Property 'description' does not exist on type D
}
CodePudding user response:
You could write matches()
as a user-defined type guard function so that the compiler understands your intent for the true
/false
output of the function to narrow the type of the t
parameter.
Here's one way to do it:
function matches<T extends { type: string }, K extends string>(
t: T, types: K[]
): t is Extract<T, { type: K }> {
const widenedTypes: readonly string[] = types;
return widenedTypes.includes(t.type);
}
This is generic both in T
, the type of t
, but also K
, the union of string literal types of the elements of types
. The return type is Extract<T, {type: K}>
, which uses the Extract<T, U>
utility type to filter the union type T
to just those constituents assignable to {type: K}
.
Note that the compiler will complain about types.includes(t.type)
because the elements of types
are narrower than t.type
. The way I deal with that is to first (safely) widen types
from K[]
to readonly string[]
and then call includes()
on that. See this question and its answers for more information.
Let's see if it works:
function logger(t: D) {
if (matches(t, ['a', 'b'])) console.log(t.title) // okay
if (matches(t, ['a', 'c'])) console.log(t.description) // okay
}
Looks good!