typescript 根据键数组确定对象上可用的键

e0bqpujr  于 2023-05-01  发布在  TypeScript
关注(0)|答案(2)|浏览(138)

我正在用TypeScript编写一个API Package 器库,我希望TypeScript能够根据API调用的用户输入给予精确的返回类型信息。我还想将API响应转换为对库使用者更有用的东西。
假设我有一个返回类型为someType的API端点:

type someType = {
    statuses?: string[],  // 
    distances?: number[]  // all arrays will be of equal length
}

其中数组总是具有相同的长度并且对应于给定索引处的相同特征。所以我想把响应转换成someOtherType类型的数组:

type someOtherType = {
  status?: string,
  distance?: number
}

用户可以根据someType键数组控制响应someType上的属性。所以我想让TypeScript知道哪些属性会在返回的对象上,哪些不会。我尝试了以下方法,但没有效果:

type someTypeKeys = keyof someType
type someMappedTypeKeys = keyof someOtherType

const Mapping: Record<someTypeKeys, someMappedTypeKeys> = {
  statuses: "status",
  distances: "distance"
}

const someObj: someType = {
    statuses: ["1"],
}

type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>
type FunctionReturnType<T extends keyof someType> = Required<Pick<someOtherType, typeof Mapping[T]>>


function a<T extends (keyof someType)[]>(arg: T): Required<Pick<someOtherType, typeof Mapping[typeof arg[number]]>>[] {
  const someResponseObj = { // some imaginary response
    statuses: ["1"],
  } as unknown as Required<Pick<someType, typeof arg[number]>>

  
  const arr: Required<Pick<someOtherType, typeof Mapping[typeof arg[number]]>>[]  = []
 

  for (let i = 0; i < (someObj.statuses.length) && i ; ++i) {
      const mappedObj = arg.reduce((agg, key) => {
        const mappedKey = Mapping[key]
        const currArr = someObj[key]
        if (currArr === undefined) {
          return agg
        } else {

          return {...agg, [mappedKey]: currArr[i]}
        }
        
      }, {}) as Required<Pick<someOtherType, typeof Mapping[typeof arg[number]]>>

      arr.push(mappedObj)

  }

  return arr;
}

const b = a(["statuses"])
b[0].distance // should be undefined
b // can I make TS be able to tell the properties available on objects in array b?
soat7uwm

soat7uwm1#

给定输入类型

type SomeType = {
  statuses: string[],
  distances: number[]
}

以及Map类型

const mapping = {
  statuses: "status",
  distances: "distance"
} as const;

type Mapping = typeof mapping;

我们想将a的呼叫签名表示为

declare function a<K extends keyof SomeType>(keys: K[]): OutputType<K>[];

其中OutputType<K>表示给定K[]类型的keys集合的输出数组的元素类型。
我们将使用一些helper类型来构建它:

type MapKeys<T, M extends Record<keyof T, PropertyKey>> =
  { [K in keyof T as M[K]]: T[K] }

type PropElements<T extends { [K in keyof T]?: readonly any[] }> =
  { [K in keyof T]: Exclude<T[K], undefined>[number] };

MapKeys<T, M>使用键重Map将T的键更改为MapM中的键,而不更改值。PropElements<T>将对象的属性值-with-array-property-values更改为只保存元素类型的对象。
那么OutputType<K>就是:

type OutputType<K extends keyof SomeType> =
  MapKeys<PropElements<Pick<SomeType, K>>, Mapping>;

我们从K中选取SomeType的键,用PropElements获取它们的数组元素类型,然后用MapKeysMappingMap键。不幸的是,这将最终显示为智能感知。为了让它看起来像一个普通的对象类型,我们将使用How can I see the full expanded contract of a Typescript type?中的技巧:

type OutputType<K extends keyof SomeType> =
  MapKeys<PropElements<Pick<SomeType, K>>, Mapping> extends infer O {
    [P in keyof O]: O[P]
  } : never;

让我们测试一下:

type S = OutputType<"statuses">;
// type S = {  status: string; } 
type D = OutputType<"distances">;
// type D = {  distance: number; }
type SD = OutputType<"statuses" | "distances">;
// type SD = { status: string; distance: number; }

并确保它在调用a()时如所宣传的那样工作:

const s = a(["statuses"]);
// const s: { status: string; }[] 

const d = a(["distances"]);
// const d: { distance: number; }[] 

const sd = a(["statuses", "distances"]);
// const sd: { status: string; distance: number; }[]

这是打字。至于实现,这可能取决于您的用例;以下是一种可能方法:

function a<K extends keyof SomeType>(keys: K[]): OutputType<K>[] {

  // this comes from somewhere
  const someType: SomeType = {
    statuses: ["a", "b", "c", "d"],
    distances: [1, 2, 3, 4]
  }

  const arrLen = Math.min(...Object.values(someType).
    filter(x => x).map(x => x.length));
  const ret: any[] = [];
  for (let i = 0; i < arrLen; i++) {
    const retObj: any = {};
    for (const k of keys) {
      const ok = someType[k];
      if (ok) retObj[mapping[k]] = ok[i];
    }
    ret.push(retObj);
  }
  return ret;
}

它只是找出输出的数组长度,然后遍历输入对象的属性,并复制和Map键。我更关心的是调用签名类型,而不是实现中的正确性和类型验证,因此您应该确保进行了满足您需要的任何更改。
不过,为了完整性,下面是下面的内容:

const s = a(["statuses"]);
console.log(s);
// [{ "status": "a" }, { "status": "b" }, { "status": "c" }, { "status": "d" }] 

const sd = a(["statuses", "distances"]);
console.log(sd);
// [{ "status": "a", "distance": 1 }, { "status": "b", "distance": 2 }, 
//  { "status": "c", "distance": 3 }, { "status": "d",  "distance": 4 }] 

const d = a(["distances"]);
console.log(d);
// [{ "distance": 1 }, { "distance": 2 }, { "distance": 3 }, { "distance": 4 }]

看起来不错!
Playground链接到代码

bnl4lu3b

bnl4lu3b2#

https://tsplay.dev/NB8lgW

type someType = {
    statuses?: string[],  // 
    distances?: number[]  // all arrays will be of equal length
}

type someOtherType = {
    status?: string,
    distance?: number
}

type someTypeKeys = keyof someType
type someMappedTypeKeys = keyof someOtherType

const Mapping = {
    statuses: "status",
    distances: "distance"
} satisfies Record<someTypeKeys, someMappedTypeKeys>
// or `as const`

const someObj: someType = {
    statuses: ["1"],
}

type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>
type FunctionReturnType<T extends keyof someType> = Required<Pick<someOtherType, typeof Mapping[T]>>

function mapArrayObjectToObjectArray<
    Source extends Record<PropertyKey, any[]>,
    Mapping extends Record<keyof Source, string>,
    Keys extends keyof Source = keyof Source,
>(
    source: Source,
    mapping: Mapping,
    keys: Keys[]
): { [K in Keys as Mapping[K]]-?: Source[K][number] }[] {
    let length = Object.values(source)[0].length
    return Array.from(
        { length },
        (_, index) => {
            return Object.fromEntries(
                keys.map(k => [mapping[k], source[k][index]])
            ) as any
        }
    )
}

const someResponseObj = { // some imaginary response
    statuses: ["1"],
} satisfies someType

const b = mapArrayObjectToObjectArray(someResponseObj, Mapping, ["statuses"])
//    ^?
// const b: {
//     status: string;
// }[]
b[0].distance // should be undefined
b // can I make TS be able to tell the properties available on objects in array b?

相关问题