简体   繁体   English

TypeScript,对象和类型区分

[英]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 .它忘记了如果elementA类型,那么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类型都是安全的......既是AB的东西,或者是交集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实际上将接受AB ,无论调用者想要传递什么。这是一个谎言,但它是无害的,因为您知道该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 类型,它表示AB的判别式与其对应data类型之间的映射:

type TypeMap = { a: boolean, b: string };

Then we can define A , B , and AB in terms of TypeMap :然后我们可以根据TypeMap定义ABAB

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;
}

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM