简体   繁体   English

带可选索引器的打字稿中返回类型的问题

[英]problem with return type in typescript with optional indexer

In the following example I want to type the return type of a function: 在下面的示例中,我要键入函数的返回类型:

type MakeNamespace<T> = T & { [index: string]: T }

export interface INodeGroupProps<T = unknown[], State = {}> {
  data: T[];
  start: (data: T, index: number) =>  MakeNamespace<State>;
}

export interface NodesState {
  top: number;
  left: number;
  opacity: number;
}

export type Point = { x: number, y: number }

const points: Point[] = [{ x: 0, y: 0 }, { x: 1, y: 2 }, { x: 2, y: 3 }]

const Nodes: INodeGroupProps<Point, NodesState> = {
  data: points,
  keyAccessor: d => {
    return d.x
  },
  start: point => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
    }
  }
}

The return type of the start function could be: start函数的返回类型可以是:

    return {
      top: point.y,
      left: point.x,
      opacity: 0
    }

or it could be: 或者可能是:

return {
  namespace1: {
    top: point.y,
    left: point.x,
    opacity: 0
  },
  namespace2: {
    top: point.y,
    left: point.x,
    opacity: 0
  }
}

My code will not allow this and typescript complains: 我的代码不允许这样做,打字稿抱怨:

Property 'top' is incompatible with index signature 属性“ top”与索引签名不兼容

Changing MakeNamespace to type MakeNamespace<T> = T | { [index: string]: T } 更改MakeNamespacetype MakeNamespace<T> = T | { [index: string]: T } type MakeNamespace<T> = T | { [index: string]: T } works but won't cover this case: type MakeNamespace<T> = T | { [index: string]: T }可以解决,但不能解决这种情况:

const Nodes: INodeGroupProps<Point, NodesState> = {
  data: points,
  start: point => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0
      namespace1: {
        top: point.y,
        left: point.x,
        opacity: 0
      }
    }
  }
}

Where i have a combination of both. 在我有两者结合的地方。

Due to return type widening, I lose type safety on the namespace key because it is going for the T part of the union. 由于返回类型的扩展,我失去了namespace键的类型安全性,因为它将用于联合的T部分。

My thoughts would be to make the indexer optional but I am not sure how to do that. 我的想法是使索引器成为可选项,但我不确定如何做到这一点。

Here is a playground 这是一个游乐场

If you are in control of enough code to change the design to something more TypeScript-friendly, that would be my suggestion. 如果您控制着足够的代码来将设计更改为对TypeScript更友好的内容,那将是我的建议。 For example, if you place the extra namespaces in a single property with a known name, it will be much simpler to describe the type (note that I'm going to remove code and types that are not relevant to the issue, such as the INodeGroupProps['data'] property): 例如,如果您将额外的名称空间放在具有已知名称的单个属性中,则描述类型将更加简单(请注意,我将删除与该问题无关的代码和类型,例如INodeGroupProps['data']属性):

// add an optional "namespaces" property which is itself a bag of T
type MakeNamespace<T> = T & { namespaces?: { [k: string]: T | undefined } };

export interface INodeGroupProps<T, State> {
  start: (data: T, index: number) => MakeNamespace<State>;
}

export interface NodesState {
  top: number;
  left: number;
  opacity: number;
}

export type Point = { x: number, y: number }

// okay
const Nodes: INodeGroupProps<Point, NodesState> = {
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
    }
  }
};

// also okay
const Nodes2: INodeGroupProps<Point, NodesState> = {
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
      namespaces: {
        namespace1: {
          top: point.y,
          left: point.x,
          opacity: 0,
        }
      }
    }
  }
};

That still has the issue of widening the return type to INodeGroupProps<Point, NodesState> so the compiler will forget, for example, that Nodes2.namespaces.namespace1 exists: 仍然存在将返回类型扩展为INodeGroupProps<Point, NodesState>因此编译器将忘记例如存在Nodes2.namespaces.namespace1

const ret = Nodes2.start({ x: 1, y: 2 }, 0);
const oops = ret.namespaces.namespace1; // error! possibly undefined?
if (ret.namespaces && ret.namespaces.namespace1) {
  const ns = ret.namespaces.namespace1; // okay, ns is NodeState now
} else {
  throw new Error("why does the compiler think this can happen?!")
}

Generally the way around this is to change your possibly-too-wide type annotations to calling a helper function which requires that the value matches the annotation, but does not widen to it. 通常,解决此问题的方法是将您可能太宽的类型注释更改为调用帮助程序函数,该函数要求该值与注释匹配,但不扩展至该注释。 Like: 喜欢:

const ensure = <T>() => <U extends T>(x: U) => x;

const ensureRightNodes = ensure<INodeGroupProps<Point, NodeState>>();


const Nodes = ensureRightNodes({
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
    }
  }
});

const Nodes2 = ensureRightNodes({
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
      namespaces: {
        namespace1: {
          top: point.y,
          left: point.x,
          opacity: 0,
          timing: { duration: 500 }
        }
      }
    }
  }
});

// Now Nodes and Nodes2 are narrowed:

const ret = Nodes2.start({ x: 1, y: 2 }); // Nodes2.start takes ONE param now
const ns = ret.namespaces.namespace1; // okay, ns is NodesState.

There are benefits to both widening and narrowing (widening: Nodes2.start() will accept two parameters as defined in INodeGroupProps ; narrowing: Nodes2.start(p).namespaces.namespace1 is known to exist) so it's a tradeoff and it's up to you to figure out where the right balance is for your needs. 扩大和缩小都有好处(扩大: Nodes2.start()将接受INodeGroupProps定义的两个参数;缩小: Nodes2.start(p).namespaces.namespace1存在),所以这是一个折衷,它取决于您找出适合您需求的平衡点。


Backing up, it's possible you might need to keep the original " T plus all sorts of extra properties" definition, and in that case you can coax and cajole the compiler to accepting it using conditional types , with the same caveats about narrowing as above: 备份时,可能需要保留原始的“ T加上各种额外的属性”定义,在这种情况下,您可以哄骗并哄骗编译器使用条件类型来接受它,但要注意与上述缩小相同的注意事项:

type MakeNamespace<T, K extends keyof any> = T & Record<Exclude<K, keyof T>, T>;

export interface INodeGroupProps<T, State, K extends keyof any> {
  start: (data: T, index: number) => MakeNamespace<State, K>;
}

type ExtraKeysFromINodeGroupProp<T, State, N> =
  N extends INodeGroupProps<any, any, any> ?
  Exclude<keyof ReturnType<N['start']>, keyof State> : never

type InferINodeGroupProp<T, State, N> =
  INodeGroupProps<T, State, ExtraKeysFromINodeGroupProp<T, State, N>>;

const inferNodeGroupPropsFor = <T, State>() =>
  <N>(ingp: N & InferINodeGroupProp<T, State, N>):
    InferINodeGroupProp<T, State, N> => ingp;

const ngpPointNodesState = inferNodeGroupPropsFor<Point, NodesState>();

const Nodes = ngpPointNodesState({
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
    }
  }
});

const Node2 = ngpPointNodesState({
  start: (point: Point) => {
    return {
      top: point.y,
      left: point.x,
      opacity: 0,
      namespace1: {
        top: point.y,
        left: point.x,
        opacity: 0,
      }
    }
  }
});


Node2.start({ x: 1, y: 1 }, 0).namespace1.left; // okay

To get that working you need to make MakeNamespace generic enough to represent which extra keys K you are adding, and then do all kinds of things to verify that a passed-in value matches INodeGroupProps<T, State, K> for some K that you extract from the value's type. 要获得工作,你需要做MakeNamespace通用的,足以代表额外的钥匙K要添加,然后做各种各样的事情来验证传入的值匹配INodeGroupProps<T, State, K>对于一些K你从值的类型中提取。 It's frankly a mess... so if possible I'd really recommend trying not to go the dynamic-extra-properties route. 坦白说,这是一团糟……所以,如果可能的话,我真的建议您不要尝试使用动态额外属性路线。


Okay, hope that helps. 好的,希望对您有所帮助。 Good luck! 祝好运!

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

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