我想声明
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'
) 键的并集
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。
如果您在像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"
*当您将变量注释为 union 类型时,这并不完全正确;在这种情况下,编译器将在赋值时缩小变量的类型。但由于
Record<string, Type1>
不是联合类型,所以这不适用于当前情况。
使用显式类型会禁用不可变断言。考虑使用显式类型或
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
。