网站搜索

将 Typescript 属性设置为 `never` 仍然会将其视为必填


我正在尝试创建一种类型,该类型会在不同阶段逐步被填充。例如,在创建一个 Bar 对象时,我希望 Data<DataType.bar> 包含一个完整的 Foo 对象,但 Bar 本身或者 Baz 还不会是 Data 的一部分:

enum DataType {
  'foo',
  'bar',
  'baz',
}

type Foo = {
  a: string;
};
type Bar = {
  b: string;
};
type Baz = {
  c: string;
};

type Data<T extends DataType | null = null> = {
  [DataType.foo]: T extends DataType.bar | DataType.baz | null ? Foo : never;
  [DataType.bar]: T extends DataType.baz | null ? Bar : never;
  [DataType.baz]: T extends null ? Baz : never;
};

const myData: Data = {
  [DataType.foo]: { a: 'I should already exist while bar is being created' },
  /*
  [DataType.bar]: {
     b: `I correctly error when uncommented...
         but when commented, DataType.bar also appears to be required?`
  }
  */
};

尽管当包含 DataType.bar 时,myData 正确地包含了一个错误,但当它被注释掉时,我也会收到以下错误:

Type '{ 0: { a: string; }; }' is missing the following properties from type 'Data<DataType.bar>': [DataType.bar], [DataType.baz]

有没有办法根据 T extends DataType 泛型告诉 TypeScript 这些属性实际上并没有缺失?

编辑:这似乎可以达到我想要的效果,但看起来非常重复?有没有更好的方法来做这个?

type Data<T extends DataType | null = null> = T extends null
  ? {
      [DataType.foo]: Foo;
      [DataType.bar]: Bar;
      [DataType.baz]: Baz;
    }
  : T extends DataType.baz
  ? {
      [DataType.foo]: Foo;
      [DataType.bar]: Bar;
    }
  : T extends DataType.bar
  ? {
      [DataType.foo]: Foo;
    }
  : T extends DataType.foo
  ? {}
  : never;

答案#1

使用 interface 和 extends

enum DataType {
  'foo',
  'bar',
  'baz',
}
interface DataFoo { [x:string]: never}
interface DataBar { foo: string; }; 
interface DataBaz extends DataBar { bar: string; };
interface DataAll extends DataBaz { baz: string; };
type Data<T extends DataType | null = null> = T extends null ? DataAll
  : T extends DataType.baz
  ? DataBaz
  : T extends DataType.bar
  ? DataBar
  : T extends DataType.foo
  ? DataFoo
  : never;
const dataFoo: Data<DataType.foo> = {};
const dataBar: Data<DataType.bar> = { foo: "" };
const dataBaz: Data<DataType.baz> = { foo: "", bar: "" };
const dataAll: Data = { foo: "", bar: "", baz:"" };

(参见 TypeScript Playground)

使用 type 和类型运算符 &

enum DataType {
  'foo',
  'bar',
  'baz',
}
type DataFoo = Record<string,never>;
type DataBar = {foo:string};
type DataBaz = DataBar & {bar:string};
type DataAll = DataBaz & {baz:string};
type Data<T extends DataType | null = null> = T extends null ? DataAll
  : T extends DataType.baz
  ? DataBaz
  : T extends DataType.bar
  ? DataBar
  : T extends DataType.foo
  ? DataFoo
  : never;
const dataFoo: Data<DataType.foo> = {};
const dataBar: Data<DataType.bar> = { foo: "" };
const dataBaz: Data<DataType.baz> = { foo: "", bar: "" };
const dataAll: Data = { foo: "", bar: "", baz:"" };
    

(参见 TypeScript Playground)

`Record` 是一个 TypeScript 工具函数。`Record<string,never>` 与 `{[x:string]:never}` 相同

主要的好处是文本长度随着 O(Nlevels) 增加。在你原来的版本中,它是随着 O(Nlevels^2) 增加的。