如何在不声明键类型的情况下声明对象值类型?

问题描述 投票:0回答:2

问题陈述

我想声明

mapper1
,使其值只能为
Type1
,并声明
mapper2
,使其值只能为
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;

bar1
bar2
具有相同的类型 (
Type1
)。
bar3
bar4
具有相同的类型 (
Type2
)。
Type1
Type2
不同。

MapperKeys
mapper1
mapper2
(
'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'
。我希望它是
mapper1
mapper2
(
'foo1' | 'foo2' | 'foo3' | 'foo4'
)

键的并集

方法2:

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

typescript types
2个回答
15
投票

如果您在像const x: T这样的变量上使用

类型注释
,或者在像x as T这样的表达式上使用
类型断言
,那么您就是在告诉编译器将变量或值视为该类型。这本质上会丢弃有关编译器可能推断出的任何更具体类型的信息*。
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

相反,您正在寻找类似 TypeScript 4.9 中引入的

satisfies
运算符之类的东西。这个想法是像
x satisfies T
verify 这样的表达式可以将
x
分配给类型
T
,而无需将其扩大
T
。有了那个运算符,你可以说

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"

然后完成。


对于 4.9 之前的 TypeScript 版本,您可以编写辅助函数以实现类似的行为。一般形式是这样的:

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

然后你写(更麻烦)

x satisfies T
而不是
satisfies<T>()(x)
。这是有效的,因为
satisfies<T>()
生成
<U extends T>(u: U)=>u
形式的恒等函数,其中输入
U
的类型是 constrained
T
,并且返回类型是较窄的类型
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 代码链接


*当您将变量注释为 union 类型时,这并不完全正确;在这种情况下,编译器将在赋值时缩小变量的类型。但由于

Record<string, Type1>
不是联合类型,所以这不适用于当前情况。


0
投票

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

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

游乐场

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

© www.soinside.com 2019 - 2024. All rights reserved.