为联合类型和泛型编写巧妙的类型保护

问题描述

我有以下结构:

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>;

并且知道我经常filter碎片的挑战。我目前的方法是通过以下类型保护:

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'}}));

这工作正常,但留下了将 IFragmentData错误FragmentType 组合的选项。例如: isFragmentType<IMarkFragmentData>('Sentence') 将是有效代码,即使 'Sentence' 是 IMarkFragmentData 类型的错误鉴别器。

有没有更聪明的方法来编写我的类型保护甚至重构我的输入?

解决方法

isFragmentType() 函数的主要问题是 t 的类型根本不限于 T。我可能会重写它,以便 T 表示 type 属性,并使用 the Extract utility type 过滤具有该 Fragment 属性的成员的 type 联合:

function isFragmentType<T extends Fragment['type']>(t: T) {
  return (x: Fragment): x is Extract<Fragment,{ type: T }> => {
    return x.type === t;
  };
}

您可以验证这是否按预期工作(并且您不必手动指定 T,因为它可以从 t 的类型推断):

function processFragment(f: Fragment) {
  if (isFragmentType("Word")(f)) {
    f.data.word.toUpperCase(); // okay
  } else {
    f.data.sentence.toUpperCase(); // okay
  }
}

仅供参考,我不确定为什么 isFragmentType()curried,但它看起来并不需要:

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

Playground link to code