簡體   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