繁体   English   中英

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

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

在下面的示例中,我要键入函数的返回类型:

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

start函数的返回类型可以是:

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

或者可能是:

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

我的代码不允许这样做,打字稿抱怨:

属性“ top”与索引签名不兼容

更改MakeNamespacetype MakeNamespace<T> = T | { [index: string]: T } 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
      }
    }
  }
}

在我有两者结合的地方。

由于返回类型的扩展,我失去了namespace键的类型安全性,因为它将用于联合的T部分。

我的想法是使索引器成为可选项,但我不确定如何做到这一点。

这是一个游乐场

如果您控制着足够的代码来将设计更改为对TypeScript更友好的内容,那将是我的建议。 例如,如果您将额外的名称空间放在具有已知名称的单个属性中,则描述类型将更加简单(请注意,我将删除与该问题无关的代码和类型,例如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,
        }
      }
    }
  }
};

仍然存在将返回类型扩展为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?!")
}

通常,解决此问题的方法是将您可能太宽的类型注释更改为调用帮助程序函数,该函数要求该值与注释匹配,但不扩展至该注释。 喜欢:

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.

扩大和缩小都有好处(扩大: Nodes2.start()将接受INodeGroupProps定义的两个参数;缩小: Nodes2.start(p).namespaces.namespace1存在),所以这是一个折衷,它取决于您找出适合您需求的平衡点。


备份时,可能需要保留原始的“ 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

要获得工作,你需要做MakeNamespace通用的,足以代表额外的钥匙K要添加,然后做各种各样的事情来验证传入的值匹配INodeGroupProps<T, State, K>对于一些K你从值的类型中提取。 坦白说,这是一团糟……所以,如果可能的话,我真的建议您不要尝试使用动态额外属性路线。


好的,希望对您有所帮助。 祝好运!

暂无
暂无

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

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