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

Writing clever type guards for union type and generics

我有以下结构:

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 联合 [=17] 的成员=] 属性:

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