【问题标题】:Constrain object keys to enum's values while not requiring all keys to be present and object values are not considered undefined将对象键约束为枚举值,而不要求所有键都存在,并且对象值不被视为未定义
【发布时间】:2021-05-21 06:38:36
【问题描述】:

这里有很多类似的问题,但它们没有涵盖帖子末尾的清单(尤其是“未定义的值”部分)。

我有一个枚举

enum E {
  A = 'A',
  B = 'B'
}

然后我想将状态中的对象值限制为{ [key in E]: string },但这需要我实例化对象已经将所有枚举的键。这是不允许的:

const state: { [key in E]: string } = {};

类型“{}”缺少类型“{ A: string;”中的以下属性 B:字符串; }':A,B

所以我试着像{ [key in E]?: string } 那样约束它。这允许我省略枚举键,从而允许实例化空对象 {} 并检查键值是否在枚举范围内:

state.A = 'x'; // ok
state.C = 'y'; // gives error which is nice

但后来我在 forEaching Object 条目时遇到了问题

Object.entries(state).forEach(([key, value]) => console.log(value === undefined));

Typescript 认为 value 可以是 string|undefined 类型,但事实并非如此。

仅使用字符串作为键时,该值不被视为未定义

const state: { [key: string]: string } = {};

Playground example


我如何将对象键约束为枚举值,而不需要它们的存在并且值不是未定义的?

清单:

state.A = 'x' // ok
state.C = 'x' // error
Object.entries(state).forEach(([key, value])=>console.log(value.charAt(0))); // ok, no Object is possibly 'undefined'.(2532) error

TLDR:我想去掉Example here中的assert

【问题讨论】:

    标签: typescript enums


    【解决方案1】:

    就打字稿而言,从对象中省略A 与将A 显式设置为undefined 相同。这样做很好:

    const state: { [key in E]?: string } = {};
    state.A = undefined;
    

    @captain-yossarian 为您提供了一种很好但非常复杂的方法来防止这种行为。更简单的做法是简单地过滤掉任何 undefined 值。

    此回调是一个type guard,它确保定义了[key, value] 元组中的value

    const isDefinedValue = <T,>(tuple: [string, T | undefined]): tuple is [string, Exclude<T, undefined>] => {
      return tuple[1] !== undefined;
    }
    

    然后我们可以在使用之前过滤Object.entries

    Object.entries(state)
      .filter(isDefinedValue)
      .forEach(([key, value])=>console.log(value.charAt(0)));  // value is `string`
    

    【讨论】:

    • 这就是我所说的:跳出框框思考
    • @captain-yossarian 您的回答完全符合他们的要求,遗憾的是这样做太复杂并且有局限性。我不知道是否有更好的方法来定义他们想要的状态。可能不会。
    • 是的,越简单越好。我试图重构我的答案,但没有成功。就我个人而言,我正在尝试尽可能多地使用类型保护
    【解决方案2】:
    enum E {
      A = 'A',
      B = 'B'
    }
    
    type O = keyof typeof E
    const state: { [key in keyof typeof E]?: string } = {};
    

    如果你想让state 可变,只需使用-readonly

    const state: { -readonly [key in keyof typeof E]?: string } = {};
    
    

    我如何将对象键约束为枚举值,而不需要它们的存在并且值不是未定义的?

    能否提供一些伪代码?

    因为如果不需要key,你会尝试获取不存在key的值,根据JS标准你应该收到undefined

    更新

    enum E {
      A = 'A',
      B = 'B',
      C = 'C',
      D = 'D'
    }
    
    // credits goes to https://stackoverflow.com/a/50375286
    type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
      k: infer I
    ) => void
      ? I
      : never;
    
    // //https://github.com/microsoft/TypeScript/issues/13298#issuecomment-468114901
    type UnionToOvlds<U> = UnionToIntersection<
      U extends any ? (f: U) => void : never
    >;
    
    type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;
    
    type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
    
    type UnionToArray<T, A extends unknown[] = []> = IsUnion<T> extends true
      ? UnionToArray<Exclude<T, PopUnion<T>>, [PopUnion<T>, ...A]>
      : [T, ...A];
    
    type State<T extends string, R extends string[] = []> = {
      [P in T]: IsUnion<T> extends true ? State<Exclude<T, P>, [...R, Exclude<T, P>]> : R
    }[T]
    
    // Array of all possible keys
    type Result = UnionToArray<State<keyof typeof E>>
    
    // convert union to object with appropriate keys
    type MapPredicate<T> = UnionToIntersection<T extends string ? {
      [P in T]: string
    } : never>
    
    // don't allow empty object because value can't be undefined
    type Empty = { __tag: 'empty' }
    
    // iterate through array of strings
    type MappedString<
      Arr,
      Result = Empty
      > = Arr extends []
      ? []
      : Arr extends [infer H]
      ? Result | MapPredicate<H>
      : Arr extends [infer Head, ...infer Tail]
      ? MappedString<[...Tail], Result | MapPredicate<Head>>
      : Readonly<Result>;
    
    
    // iterate through array of array of string
    type MappedArray<
      Arr extends Array<unknown>,
      Result extends Array<unknown> = []
      > = Arr extends []
      ? []
      : Arr extends [infer H]
      ? [...Result, MappedString<H>]
      : Arr extends [infer Head, ...infer Tail]
      ? MappedArray<[...Tail], [...Result, MappedString<Head>]>
      : Readonly<Result>;
    
    type AllPossibleValues = MappedArray<Result>[number];
    
    const A: AllPossibleValues = { A: 'A' }
    const AB: AllPossibleValues = { A: 'A', B: 'B' }
    const ABC: AllPossibleValues = { A: 'A', B: 'B', C: 'C' }
    const ABCD: AllPossibleValues = { A: 'A', B: 'B', C: 'C', D: 'D' }
    const CD: AllPossibleValues = { C: 'C', D: 'D' }
    const AD: AllPossibleValues = { A: 'A', D: 'D' }
    const BD: AllPossibleValues = { B: 'B', D: 'D' }
    
    const BE: AllPossibleValues = {} // expected error
    const QA: AllPossibleValues = {A:'A', Q:'Q'} // expected error
    
    const state:AllPossibleValues={A:'A'}
    
    
    const x = Object.entries(state).forEach(([key, value]) => { /* [key: string, value: string] */
    
    })
    
    

    优点:没有断言,没有类型转换

    缺点:我必须使用类似的映射实用程序,但我不知道如何重构。但无论如何它不会影响您编译的代码。另外,如果你在枚举中添加第 5 个属性,上面的代码将无法编译,因为递归限制:)

    因此,如果您的对象的道具少于 5 个,那么您就可以开始了。

    TypeScript 允许您大约 50 次递归调用。

    如果你有 obj 有 5 个 props,你应该用 parseInt('11111',2) 31 个项目创建 union。我想是因为我的 rec MappedArray 调用了 rec MappedString 我更快地达到了这个限制。

    Playground link

    【讨论】:

    • 也许它还没有在 typescript 中实现?但是,如果我正在循环条目(键:值对)Object.entries(state),如何获得不存在键的值?
    • codesandbox.io/s/empty-glitter-ps18k?file=/src/index.ts 我必须使用 assert 即使使用 Object.entries() 应该 IMO 排除所有 undefined 值。
    • @simPod 请看一下,我更新了
    • 哇,详尽!谢谢并投票!虽然我有点失望,因为我认为我只是错过了一些东西。在某处忘记?! ? 虽然看起来这不是微不足道的。如果你不介意的话,我会在口袋里多放一段时间接受糖果。
    • 当然,如果有人能找到更好的方法来处理它,我会很高兴。
    猜你喜欢
    • 2019-08-16
    • 1970-01-01
    • 1970-01-01
    • 2020-09-06
    • 1970-01-01
    • 1970-01-01
    • 2021-05-26
    • 2021-04-11
    • 1970-01-01
    相关资源
    最近更新 更多