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

时间:2019-03-06 08:34:27

标签: typescript

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

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”与索引签名不兼容

MakeNamespace更改为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部分。

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

这里是playground

1 个答案:

答案 0 :(得分:0)

如果您控制着足够的代码来将设计更改为对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以及各种额外的属性”定义,在这种情况下,您可以哄骗并哄骗编译器使用conditional types接受它,但缩小范围的注意事项与上述相同:

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匹配。坦率地说,这是一团糟...所以,如果可能的话,我真的建议您不要尝试使用动态额外属性路线。


好的,希望能有所帮助。祝你好运!