I have following structure:
const fragmentTypes = [
'Word',
'Sentence',
] as const;
type FragmentType = typeof fragmentTypes[number];
interface IFragmentData {
type: FragmentType;
}
interface IFragment<T extends IFragmentData> {
id: string;
type: T['type'];
data: Omit<T, 'type'>;
}
interface IWordFragmentData extends IFragmentData {
type: 'Word';
word: string;
}
interface ISentenceFragmentData extends IFragmentData {
type: 'Sentence';
sentence: string;
}
type Fragment =
| IFragment<IWordFragmentData>
| IFragment<ISentenceFragmentData>;
and know have the challenge that I often filter
Fragments. My current way is by the following type guard:
function isFragmentType<T extends IFragmentData>(t: FragmentType) {
return (x: Fragment | IFragment<T>): x is IFragment<T> => {
return x.type === t;
};
}
console.log(isFragmentType<IWordFragmentData>('Word')({type: 'Word', id: 'test123', data: {word: 'test123'}}));
This works fine but leaves the option to combine a IFragmentData
with the wrong FragmentType
. For example : isFragmentType<IMarkFragmentData>('Sentence')
would be valid code even though 'Sentence' would be the wrong discriminator for the IMarkFragmentData
type.
Is there a smarter way to write my type guard or even to restructure my typing?
The main problem with your isFragmentType()
function is that the type of t
is not constrained at all to T
. I'd probably rewrite it so that T
represents the type
property, and use the Extract
utility type to filter the Fragment
union for the member with that type
property:
function isFragmentType<T extends Fragment['type']>(t: T) {
return (x: Fragment): x is Extract<Fragment, { type: T }> => {
return x.type === t;
};
}
You can verify that this works as desired (and you don't have to manually specify T
since it can be inferred from the type of t
):
function processFragment(f: Fragment) {
if (isFragmentType("Word")(f)) {
f.data.word.toUpperCase(); // okay
} else {
f.data.sentence.toUpperCase(); // okay
}
}
FYI, I'm not sure why isFragmentType()
is curried, but it doesn't look like it needs to be:
function isFragmentType<T extends Fragment['type']>(
t: T, x: Fragment
): x is Extract<Fragment, { type: T }> {
return x.type === t;
}
function processFragment(f: Fragment) {
if (isFragmentType("Word", f)) {
f.data.word.toUpperCase(); // okay
} else {
f.data.sentence.toUpperCase(); // okay
}
}