【问题标题】:How do I declare object value type without declaring key type?如何在不声明键类型的情况下声明对象值类型?
【发布时间】:2022-02-05 09:49:20
【问题描述】:

问题陈述

我想声明 mapper1 以便它的值只能是 Type1mapper2 以便它的值只能是 Type2。在不声明密钥类型的情况下如何做到这一点?

背景

在 TypeScript 中,我有:

import Bar1 from './bar1'; // Type1
import Bar2 from './bar2'; // Type1
import Bar3 from './bar3'; // Type2
import Bar4 from './bar4'; // Type2

const mapper1 = {
  foo1: bar1,
  foo2: bar2,
} as const;
const mapper2 = {
  foo3: bar3,
  foo4: bar4,
} as const;
export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;

bar1bar2 具有相同的类型 (Type1)。 bar3bar4 具有相同的类型 (Type2)。 Type1Type2 不同。

MapperKeysmapper1mapper2 ('foo1' | 'foo2' | 'foo3' | 'foo4') 的键的并集。

我尝试了什么

方法一:

const mapper1: Record<string, Type1> = {
  foo1: bar1,
  foo2: bar2,
} as const;
const mapper2: Record<string, Type2> = {
  foo3: bar3,
  foo4: bar4,
} as const;

但现在MapperKeys'string'。我希望它是 mapper1mapper2 ('foo1' | 'foo2' | 'foo3' | 'foo4') 的键的联合

方法二:

const mapper1: Record<'foo1' | 'foo2', Type1> = {
  foo1: bar1,
  foo2: bar2,
} as const;
const mapper2: Record<'foo3' | 'foo4', Type2> = {
  foo3: bar3,
  foo4: bar4,
} as const;

这可行,但不是DRY

【问题讨论】:

  • 请提供minimal reproducible example,清楚地表明您面临的问题。理想情况下,有人可以将代码粘贴到像 The TypeScript Playground (link here!) 这样的独立 IDE 中,然后立即着手解决问题,而无需首先重新创建它。所以不应该有伪代码、拼写错误、不相关的错误或未声明的类型或值。
  • this approach 是否满足您的需求?如果你用对象类型注释一个变量,你告诉编译器它不应该尝试推断任何更具体的东西(例如,如果你注释为Record&lt;string, Type1&gt;,这意味着你以后可以写mapper1.someOtherKey = bar1,所以编译器不能说mapper1 的类型只有键 "foo1""foo2"。)如果你想要推理和约束检查,你可以编写一个通用的帮助函数来做到这一点,如我上面的链接所示。如果这是有道理的,我可以写一个答案;如果没有,我错过了什么?
  • @jcalz 这种方法看起来不错。您能否将其发布为答案以便可以接受?但是,我不明白您的解释,请您详细说明并提供相关文档的链接?

标签: typescript types


【解决方案1】:

如果您在像const x: T 这样的变量上使用type annotation,或者在像x as T 这样的表达式上使用type assertion,那么您就是在告诉编译器将变量或值视为该类型。这实质上会丢弃有关编译器可能推断出的任何更具体类型的信息*。 x 的类型将扩大T

const badMapper1: Record<string, Type1> = { foo1: bar1, foo2: bar2 };
const badMapper2 = { foo3: bar3, foo4: bar4 } as Record<string, Type2>;    

export type BadMapperKeys = keyof typeof badMapper1 | keyof typeof badMapper2;
// type BadMapperKeys = string

相反,您正在寻找 microsoft/TypeScript#7481 中要求的“satisfies 运算符”之类的东西。这个想法是,像x satisfies T 这样的表达式将验证 x 可以分配给类型T 而不用扩大它到T。如果存在这样的运算符,您可以这样说

// INVALID TYPESCRIPT, DON'T TRY IT, IT WON'T WORK:
const mapper1 = { foo1: bar1, foo2: bar2 } satisfies { [key: string]: Type1 }
const mapper2 = { foo3: bar3, foo4: bar4 } satisfies { [key: string]: Type2 }

export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;
// type MapperKeys = "foo1" | "foo2" | "foo3" | "foo4"

然后完成。不幸的是,没有直接支持这样的运算符。


幸运的是,您可以编写帮助函数来实现类似的行为。一般形式是这样的:

const satisfies = <T,>() => <U extends T>(u: U) => u;

然后你写的不是x satisfies T(更麻烦)satisfies&lt;T&gt;()(x)。这是因为satisfies&lt;T&gt;() 产生了&lt;U extends T&gt;(u: U)=&gt;u 形式的恒等函数,其中输入U 的类型是constrainedT,返回类型是较窄的类型U,而不是较宽的类型T.

让我们试试吧:

const mapper1 = satisfies<Record<string, Type1>>()({ foo1: bar1, foo2: bar2 });
const mapper2 = satisfies<Record<string, Type2>>()({ foo3: bar3, foo4: bar4 });
export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;
// type MapperKeys = "foo1" | "foo2" | "foo3" | "foo4"

看起来不错!


在您的情况下,您特别要求指定对象值类型而不是键。如果需要,您可以调整 satisfies 函数,以便指定属性值类型 T 并让编译器仅推断键。像这样的:

const satisfiesRecord = <T,>() => <K extends PropertyKey>(rec: Record<K, T>) => rec;

你可以看到它的行为类似:

const mapper1 = satisfiesRecord<Type1>()({ foo1: bar1, foo2: bar2, });
const mapper2 = satisfiesRecord<Type2>()({ foo3: bar3, foo4: bar4, });
export type MapperKeys = keyof typeof mapper1 | keyof typeof mapper2;
// type MapperKeys = "foo1" | "foo2" | "foo3" | "foo4"

Playground link to code


*当您将变量注释为union type 时,这并不严格;在这种情况下,编译器将narrow the type of the variable upon assignment。但是由于Record&lt;string, Type1&gt;不是联合类型,所以这不适用于目前的情况。

【讨论】:

    【解决方案2】:

    使用显式类型会禁用不可变断言。考虑使用显式类型或as const 断言。

    为了实现所需的行为,您应该使用辅助函数和静态验证:

    type Type1 = { type: 1 }
    type Type2 = { type: 2 }
    
    const bar1: Type1 = { type: 1 }
    const bar2: Type1 = { type: 1 }
    
    const bar3: Type2 = { type: 2 }
    const bar4: Type2 = { type: 2 }
    
    // credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
    type UnionToIntersection<U> =
      (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
    
    // credits goes to https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union
    type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true
    
    type IsValueValid<Obj> = Obj extends Record<infer _, infer Value> ? IsUnion<Value> extends true ? never : Obj : never
    
    const builder = <Key extends string, Value>(obj: IsValueValid<Record<Key, Value>>) => obj
    
    const result1 = builder({
      foo1: bar1,
      foo2: bar2,
    }) // ok, all values have same type
    
    result1.foo1 // ok
    result1.foo2 // ok
    
    const result2 = builder({
      foo1: bar3,
      foo2: bar4,
    }) // ok, all values have same type
    
    const result3 = builder({
      foo1: bar1,
      foo2: bar4,
    }) // expected error, values have different type
    

    Playground

    IsUnion - 检查对象值类型是否为联合。如果值具有不同的类型,那么我们应该将此对象视为无效对象。这正是我们在IsValueValid 中所做的。如果提供的参数不符合我们的要求,此实用程序类型返回 never

    【讨论】:

    • 我认为 OP 希望 result1 特别具有 Type1 值,而不仅仅是“相同类型”的值。但也许我看错了?
    • @jcalz 老实说我不知道​​。
    • 多么复杂的解决方案...看起来keyof typeof result1 | keyof typeof result2 确实可以根据需要解析为'foo1' | 'foo2' | 'foo3' | 'foo4'。我认为这比要求的要多,这可以解释为什么它看起来如此复杂。
    猜你喜欢
    • 1970-01-01
    • 2013-10-12
    • 2016-01-13
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-02
    相关资源
    最近更新 更多