来自 Array 的 Typescript 映射类型

Typescript mapped type from Array

只是一个示例函数:

// Merges objects | arrays
function merge(...values) {
  return Object.assign(
    {},
    ...values.map((value) =>
      Array.isArray(value)
        ? Object.fromEntries(value.map((val) => [val, null]))
        : value,
    ),
  )
}

merge({k1: 1}, {k2: 2}) // {k1: 1, k2: 2} - 
merge({k1: 1}, ['k2'])   // {k1: 1, k2: null} - 

我正在尝试弄清楚如何为函数编写类型并保持结果的结构

// Types definition
export type MixType<T> = T extends string[]
  ? { [K in T[number]]: null }
  : { [K in Extract<keyof T, string>]: T[K] }

type Test1 = MixType<{k1: 1}> // Type is: {k1: 1} - 
type Test2 = MixType<['k1']>   // Type is: {k1: null} - 

// Bind types into the function
function merge<V1>(v: V1): MixType<V1>
function merge<V1, V2>(v1: V1, v2: V2): MixType<V1> & MixType<V2>
function merge(...values) { // ... }

const t1 = merge({k1: 1}, {k2: 2}) // typeof t1: {k1: number} & {k2: number} - 
const t2 = merge({k1: 1}, ['k2']) // typeof t2: {k1: number} & {[x:string]: null} - ‍♂️
const t3 = merge(['k1']) // typeof t3: {[x: string]: null} - ‍♂️

如何让打字稿保持数组的结果结构?我怎么能理解 T[number]Extract<keyof T, string> 都产生了一个联合。所以在这两种情况下它必须是相同的 {[K in <Union>} 。但是对于数组,ts 会丢弃结果结构。

所以有疑问:

  1. 如何使 merge({k1: 1}, ['k2']) 获得 {k1: number} & {k2: null}
  2. 的类型
  3. 如何让它变得更好:merge({k1: 1}, ['k2']) 获取 {k1: 1} & {k2: null}
  4. 的类型

综合回答

基于@TadhgMcDonald-Jensen 的回复和@TitianCernicova-Dragomir 的评论

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I,
) => void
  ? I
  : never

type MixType<T> = T extends readonly string[]
  ? { [K in T[number]]: null }
  : { [K in keyof T]: T[K] }

function merge<
  Vs extends Array<S[] | Record<S, V>>,
  S extends string,
  V extends string | number | boolean | object,
>(...values: Vs): UnionToIntersection<MixType<Vs[number]>> {
  return Object.assign(
    {},
    ...values.map((value) =>
      Array.isArray(value)
        ? Object.fromEntries(value.map((val) => [val, null]))
        : value,
    ),
  )
}

const t1 = merge({ k1: 1 }, { k2: '2' })
// typeof t1: { k1: 1} & {k2: '2'} - 

const t2 = merge({ k1: true }, ['k2'])
// typeof t2: { k2: null} & {k1: true} - 

Typescript 在不将字符串文字作为泛型类型时出错,除非它是直接泛型:playground

function takeString<T extends string>(a:T): [T,T] {return [a,a]}
function takeAny<T>(a:T): [T,T] {return [a,a]}
function takeListOfStr<L extends string[]>(a:L): L {return a}

const typedAsSpecificallyHello = takeString("hello")
//  typed as ["hello", "hello"]
const typedAsString = takeAny("hello")
//  typed as [string, string]
const evenWorse = takeListOfStr(["hello", "hello"])
// typed just as string[]

这是有道理的,如果字符串列表出现在某处,则有理由假设您放置在那里的特定文字实际上并不重要,它只是一个字符串列表。但是 as const 完全覆盖此行为:playground

function readsListOfStringsWithoutModifying<T extends readonly string[]>(a:T){return a}

const tt = readsListOfStringsWithoutModifying(["a", "a"] as const)

由于您的函数确实保证传递的数据未被修改,因此您不会破坏任何类型脚本的内部结构,并且设置泛型以接受只读数组并不难。所以你会想做这样的事情:playground

type UnionToIntersection<U> = // stolen from 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type VALID_ARG = {[k:string]:unknown} | (readonly string[])
// Types definition
export type MixType<T extends VALID_ARG> = T extends readonly string[]
  ? Record<T[number], null>
  // here we are removing any readonly labels since we are creating a new object that is mutable
  // you could also just use `T` on this line if you are fine with readonly sticking around.
  : {-readonly [K in keyof T]: T[K] }

// Bind types into the function
function merge<Vs extends VALID_ARG[]>(...values:Vs): UnionToIntersection<MixType<Vs[number]>> {
    return Object.assign({}, ...values.map(
        (value) => Array.isArray(value)
                    ? Object.fromEntries(value.map((val) => [val, null]))
                    : value,
    ))
}

const t1 = merge({k1: 1}, {k2: 2})
//  this no longer  keeps 1,2, just stays `number`
const t2 = merge({k1: 1} as const, ['k2'] as const) 
// but adding `as const` makes everything retained

这里发生了一些事情,首先是泛型被限制为只能是 readonly string[] 或带有字符串键的对象,这简化了您之前的一些过滤逻辑,其次函数采用这些对象的列表作为泛型,并将 Vs[number] 传递给 MixType,这会获取所有参数的并集,以便在条件类型上进行分发,返回部分对象类型的并集,然后使用(有人 hacky ) UnionToIntersection 我们得到由 Vs[number] 生成的原始并集来表示所有部分对象的交集。