[英]TypeScript, objects and type discrimination
So typescript has something called type discrimination:所以 typescript 有一种叫做类型歧视的东西:
https://www.typescriptlang.org/play#example/discriminate-types https://www.typescriptlang.org/play#example/discriminate-types
however, is something like this possible?但是,这样的事情可能吗?
type A = {id: "a", value: boolean};
type B = {id: "b", value: string};
type AB = A | B;
const Configs = {
a: {
renderer: (data: A)=>{
// ...
}
},
b: {
renderer: (data: B)=>{
// ...
}
}
} as const;
const renderElement = (element: AB)=>{
const config = Configs[element.id];
const renderer = config.renderer;
return renderer(element);
}
Currently my last line fails with this message.目前我的最后一行因这条消息而失败。 I more or less know what it means but I'm not sure how to fix it.
我或多或少知道这意味着什么,但我不知道如何解决它。
Argument of type 'AB' is not assignable to parameter of type 'never'.
The intersection 'A & B' was reduced to 'never' because property 'id' has conflicting types in some constituents.
Type 'A' is not assignable to type 'never'.(2345)
Longer example It only shows the idea that I'm working on (I didn't event check if it works, recursive typing is prob wrong too, I didn't learn it yet).更长的例子它只显示了我正在研究的想法(我没有事件检查它是否有效,递归输入也可能是错误的,我还没有学习它)。
type HeaderProps = { text: string, size: number, color: string};
type ImageProps = { src: string, width: number, height: string};
type GroupProps = { numberOfColumns: number, children: Item[], height: string};
type Item = {
kind: string,
data: HeaderProps | ImageProps | GroupProps
}
const itemsConfig = {
header: {
onRender: (props: HeaderProps)=>{
}
// + some other handlers...
},
image: {
onRender: (props: ImageProps)=>{
}
// + some other handlers...
},
group: {
onRender: (props: GroupProps)=>{
}
// + some other handlers...
}
} as const;
const exampleItemsPartA = [
{
kind: 'header',
data: {
text: 'Hello world',
size: 16,
}
},
{
kind: 'header',
data: {
text: 'Hello world smaller',
size: 13,
color: 'red'
}
},
{
kind: 'image',
data: {
src: 'puppy.png',
width: 100,
height: 100,
}
}
];
const exampleItems = [
...exampleItemsPartA,
{
kind: 'group',
data: exampleItemsPartA
}
]
const renderItems = (items: Item[])=>{
const result = [];
for(const item of items){
const {onRender} = itemsConfig[item.kind];
result.push(onRender(item.data))
}
return result;
}
For both your original and extended examples, you are trying to represent something like a "correlated union type" as discussed in microsoft/TypeScript#30581 .对于您的原始示例和扩展示例,您都试图表示类似microsoft/TypeScript#30581中讨论的“相关联合类型”之类的东西。
For example, inside renderElement
, the renderer
value is a union of functions, while the element
value is a union of function arguments.例如,在
renderElement
内部, renderer
值是函数的联合,而element
值是 function arguments 的联合。 But the compiler won't let you call renderer(element)
because it doesn't realize that those two unions are correlated to each other.但是编译器不会让你调用
renderer(element)
因为它没有意识到这两个联合是相互关联的。 It has forgotten that if element
is of type A
then renderer
will accept a value of type A
.它忘记了如果
element
是A
类型,那么renderer
将接受A
类型的值。 And since it has forgotten the correlation, it sees any call of renderer()
as calling a union of functions .而且由于它忘记了相关性,因此它将任何对
renderer()
的调用都视为调用函数的联合。 The only thing the compiler will accept as an argument is something which would be safe for every function type in the union... something which is both an A
and a B
, or the intersection A & B
, which reduces to never
because it's not possible for something to be both an A
and a B
:编译器唯一可以接受的参数是对于联合中的每个
never
类型都是安全的......既是A
又是B
的东西,或者是交集A & B
,因为它不是某物可能既是A
又是B
:
const renderElement = (element: AB) => {
const config = Configs[element.id];
const renderer = config.renderer;
/* const renderer: ((data: A) => void) | ((data: B) => void) */
return renderer(element); // error, element is not never (A & B)
}
Anyway, in both cases, the easiest thing to do is to use a type assertion to tell the compiler not to worry about its inability to verify type safety:无论如何,在这两种情况下,最简单的做法是使用类型断言来告诉编译器不要担心它无法验证类型安全:
return (renderer as (data: AB) => void)(element); // okay
Here you're telling the compiler that renderer
will actually accept A
or B
, whatever the caller wants to pass in. This is a lie, but it's harmless because you know that element
will turn out to be the type that renderer
really wants.在这里,您告诉编译
renderer
实际上将接受A
或B
,无论调用者想要传递什么。这是一个谎言,但它是无害的,因为您知道该element
将成为renderer
真正想要的类型。
Until very recently that would be the end of it.直到最近,这将是它的结束。 But microsoft/TypeScript#47109 was implemented to provide a way to get type-safe correlated unions.
但是实现了microsoft/TypeScript#47109以提供一种获得类型安全相关联合的方法。 It was just merged into the main branch of the TypeScript code base, so as of now it looks like it will make it into the TypeScript 4.6 release.
它刚刚被合并到 TypeScript 代码库的主分支中,所以现在看来它将进入 TypeScript 4.6 版本。 We can use nightly
typescript@next
releases to preview it.我们可以使用每晚的
typescript@next
版本来预览它。
Here's how we'd rewrite your original example code to use the fix.以下是我们如何重写您的原始示例代码以使用该修复程序。 First, we write an object type which represents a mapping between the discriminant of
A
and B
and their corresponding data
types:首先,我们编写一个 object 类型,它表示
A
和B
的判别式与其对应data
类型之间的映射:
type TypeMap = { a: boolean, b: string };
Then we can define A
, B
, and AB
in terms of TypeMap
:然后我们可以根据
TypeMap
定义A
、 B
和AB
:
type AB<K extends keyof TypeMap = keyof TypeMap> =
{ [P in K]: { id: P, value: TypeMap[P] } }[K];
This is what is being called "a distributive object type".这就是所谓的“分布式 object 类型”。 Essentially we are taking
K
, a generic type parameter constrained to the discriminant values, splitting it up into its union members P
, and distributing the operation {id: P, value: TypeMap[P]}
over that union.本质上,我们采用
K
,一个受限于判别值的泛型类型参数,将其拆分为联合成员P
,并在该联合上分配操作{id: P, value: TypeMap[P]}
。
Let's make sure that works:让我们确保它有效:
type A = AB<"a">; // type A = { id: "a"; value: boolean; }
type B = AB<"b"> // type B = { id: "b"; value: string; }
type ABItself = AB // { id: "a"; value: boolean; } | { id: "b"; value: string; }
(Note that when we write AB
without a type parameter, it uses the default of keyof TypeMap
which is just the union "a" | "b"
.) (请注意,当我们编写
AB
时不带类型参数,它使用默认的keyof TypeMap
,即联合"a" | "b"
。)
Now, for configs
, we need to annotate it as being of a similarly mapped type which turns TypeMap
into a version where each property K
has a renderer
property that is a function accepting AB<K>
:现在,对于
configs
,我们需要将其注释为类似映射的类型,这会将TypeMap
转换为每个属性K
具有renderer
属性的版本,该属性是 function 接受AB<K>
:
const configs: { [K in keyof TypeMap]: { renderer: (data: AB<K>) => void } } = {
a: { renderer: (data: A) => { } },
b: { renderer: (data: B) => { } }
};
This annotation is crucial.这个注释很关键。 Now the compiler can detect that
AB<K>
and configs
are related to each other.现在编译器可以检测到
AB<K>
和configs
是相互关联的。 If you make renderElement
a generic function in K
, then the call succeeds because a function accepting AB<K>
will accept a value of type AB<K>
:如果您在
K
中将renderElement
设为通用 function ,则调用成功,因为接受AB<K>
AB<K>
的值:
const renderElement = <K extends keyof TypeMap>(element: AB<K>) => {
const config = configs[element.id];
const renderer = config.renderer;
return renderer(element); // okay
}
Now there's no error.现在没有错误了。 And you should be able to call
renderElement
and have the compiler infer K
based on the value you pass in:您应该能够调用
renderElement
并让编译器根据您传入的值推断K
:
renderElement({ id: "a", value: true });
// const renderElement: <"a">(element: { id: "a"; value: boolean; }) => void
renderElement({ id: "b", value: "okay" });
// const renderElement: <"b">(element: { id: "b"; value: string; }) => void
So that's the gist of it.这就是它的要点。 For your extended example you can write your types like
对于您的扩展示例,您可以编写您的类型,例如
type ItemMap = { header: HeaderProps, image: ImageProps, group: GroupProps };
type Item<K extends keyof ItemMap = keyof ItemMap> = { [P in K]: { kind: P, data: ItemMap[P] } }[K]
type Whatever = { a: any };
const itemsConfig: { [K in keyof ItemMap]: { onRender: (props: ItemMap[K]) => Whatever } } = {
header: { onRender: (props: HeaderProps) => { return { a: props } } },
image: { onRender: (props: ImageProps) => { return { a: props } } },
group: { onRender: (props: GroupProps) => { return { a: props } } }
};
And your renderItems()
function also needs a slight refactoring so that each item
can be passed to a generic callback:并且您的
renderItems()
function 还需要稍微重构,以便可以将每个item
传递给通用回调:
const renderItems = (items: Item[]) => {
const result: Whatever[] = [];
items.forEach(<K extends keyof ItemMap>(item: Item<K>) => {
const { onRender } = itemsConfig[item.kind];
result.push(onRender(item.data))
})
return result;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.