【问题标题】:Transform array of objects to single object and map types in TypeScript在 TypeScript 中将对象数组转换为单个对象和映射类型
【发布时间】:2021-09-27 20:21:00
【问题描述】:

几天前,我创建了一个question,关于将对象数组转换为单个对象并保留类型。但是,我提供了简化的情况,并没有解决我的问题。

我有两个班级 IntBoolArg 班级的孩子。还有2个工厂函数createIntcreateBool

class Arg<Name extends string> {

    protected name: Name

    public constructor(name: Name) {
        this.name = name
    }

}

class Int<Name extends string> extends Arg<Name> {}
class Bool<Name extends string> extends Arg<Name> {}

const createInt = <Name extends string>(name: Name) => new Int<Name>(name)
const createBool = <Name extends string>(name: Name) => new Bool<Name>(name)

现在,我想在数组和函数中定义参数(Int 或具有指定名称的 Bool),它们接受映射类型的参数(Number 用于 IntBoolean 用于 Bool ) 在论点中。

const options = {
    args: [
        createInt('age'),
        createBool('isStudent')
    ],
    execute: args => {
        args.age.length // Should be error, property 'length' does not exist on type 'number'.
        args.name // Should be error, property 'name' does not exist in 'args'.
        args.isStudent // Ok, boolean.
    }
}

我发现question 是关于映射类型的,但我不知道如何将这个args 数组映射到args 对象,所以现在我只有字符串并且丢失了关于参数类型的信息。

type Options = {
    args: Arg<string>[],
    execute: (args: Record<string, any>) => void
}

有没有办法做到这一点并在执行函数中保留参数类型?这是demo

【问题讨论】:

  • this code 是否满足您的需求?您的示例有几个问题;首先是Arg&lt;N&gt; 不知道它所代表的值的类型;我们需要解决这个问题,所以Arg&lt;N, V&gt;。那么没有与您的Options 类型完全对应的特定类型,因此我们也需要将其设为通用,Options&lt;A&gt; 其中A 对应于Arg&lt;?, ?&gt; 类型的数组。你可以按照你想要的方式进行类型推断,但这涉及到使更多的东西通用化并添加一个辅助函数。如果我应该把它写下来或者我遗漏了什么,请告诉我。
  • @jcalz 谢谢你的帮助,这正是我所需要的。请写下这个问题的答案,以便我将其标记为已解决。

标签: typescript generics


【解决方案1】:

您的示例代码存在一些问题,这些问题阻碍了您找到解决方案。我们将依次修复每个问题,直到我们有工作代码。


首先,您对Arg&lt;N&gt; 的定义仅对name 进行强类型化,对应于一个属性key。如果您希望编译器(实际上是您的代码)知道关于属性的 value 类型的任何信息,那么您需要扩展 Arg 的定义以包含它。例如:

class Arg<N extends string, V> {
  public constructor(protected name: N, protected value: V) { }
}

这里我们添加了第二个generic类型参数V来对应值的类型。此外,由于 TypeScript 的类型系统是 structural 而不是 nominal,这意味着您不能只添加一个类型参数 named V 并期望它约束任何东西。如果您希望编译器认为Arg&lt;N, V&gt; 依赖于NV,它必须在结构上weird things might happen。最简单的方法是给Arg 一个V 类型的属性。也许在您的实际代码中,Arg&lt;N, V&gt; 不会保留V 类型的值,但它可能会做与V 类型相关的某事。也许是validate(candidate: V): boolean; 之类的方法。但是对于这个例子,我只是给它一个属性。

我们现在可以扩展IntBool

class Int<N extends string> extends Arg<N, number> { }
class Bool<N extends string> extends Arg<N, boolean> { }

并且编译器肯定会明白Int&lt;N&gt; 代表number 属性,Bool&lt;N&gt; 代表boolean 属性。这种理解对于让下面的其余代码正常运行至关重要。

这也意味着我们需要对你的 create 函数做一些事情:

const createInt = <N extends string>(name: N) => new Int<N>(name, 0)
const createBool = <N extends string>(name: N) => new Bool<N>(name, false)

现在,下一个问题是 TypeScript 中没有与您的 Options 类型完全对应的 特定 类型。您可以制作一些特定的东西来接受options 的所有有效值,但不幸的是它也会接受无效值:

type TooWideOptions = {
  args: readonly Arg<string, any>[],
  execute: (args: never) => void
}

问题在于,对于任何给定类型的args 属性,execute 方法的回调参数都有一个特定的关联对象类型,而TooWideOptions 没有捕获此约束。如果不使 Options 泛型化,目前无法在 TypeScript 中捕获此约束:

type Options<A extends readonly Arg<any, any>[]> = {
  args: readonly [...A],
  execute: (args: ArgArrayToObject<A>) => void
}

现在我们有了Options&lt;A&gt;,其中A 表示args 属性的类型(Arg 元素的数组),其中execute 方法的回调参数是ArgArrayToObject&lt;A&gt; 类型,我们需要定义它。


我们怎样才能做到这一点?这是一个可能的实现:

type ArgArrayToObject<A extends readonly Arg<any, any>[]> = 
  { [T in A[number]
     as T extends Arg<infer N, any> ? N : never
  ]: T extends Arg<any, infer V> ? V : never } extends 
  infer O ? { [K in keyof O]: O[K] } : never;

此类型遍历A 的所有元素并构建mapped type whose key names are remapped。对于某些NV,每个元素的类型为Arg&lt;N, V&gt;,我们将N 设为属性键类型,将V 设为值类型。例如:

type Test = ArgArrayToObject<[
  Arg<"str", string>, 
  Arg<"num", number>, 
  Arg<"dat", Date>
]>;
/* type Test = {
    str: string;
    num: number;
    dat: Date;
} */

所以现在我们有了一个通用的Options&lt;A&gt;。如果您能以某种方式表示所有可能的A 类型的无限union,那么您可以构建您想要的特定Options 类型。但这需要所谓的存在量化的泛型,TypeScript 并不直接支持(请参阅microsoft/TypeScript#14466 获取功能请求)。

相反,我们将不得不以通常的方式保留Options&lt;A&gt; 泛型,因此您必须为Options&lt;A&gt; 类型的每个值选择一个特定的A。您可以强制开发人员手动annotate 的确切类型,包括A 的特定类型,但这既乏味又多余:

const explicitOptions: Options<[Int<"age">, Bool<"isStudent">]> = {
  args: [
    createInt('age'),
    createBool('isStudent')
  ],
  execute: args => {
    /* (parameter) args: { age: number;  isStudent: boolean;  } */
    args.age.length // error, no 'length' on number
    args.name // error, no 'name' on args
    args.isStudent // okay
  }
}

这可以随心所欲,但同时写出Options&lt;[Int&lt;"age"&gt;, Bool&lt;"isStudent"&gt;][createInt('age'), createBool('isStudent')] 并不好玩。


最后解决这个问题的方法是引入一个通用的辅助函数,让编译器为你推断A

const asOptions = <A extends readonly Arg<any, any>[]>(options: Options<A>) => options;

这个函数只是返回它的输入,所以它在运行时没有任何用途。但是在编译时,它会将options 限制为Options&lt;A&gt; 类型,因为编译器会推断出一些A

const options = asOptions({
  args: [
    createInt('age'),
    createBool('isStudent')
  ],
  execute: args => {
    /* (parameter) args: { age: number;  isStudent: boolean;  } */
    args.age.length // error, no 'length' on number
    args.name // error, no 'name' on args
    args.isStudent // okay
  }
});

您可以验证options 是否仍与之前手动注释的版本属于同一类型,而无需强制您将其写出:

// const options: Options<[Int<"age">, Bool<"isStudent">]>

就是这样。现在我们有了工作代码,可以表示和强制执行args 属性和execute 方法之间的约束,该值是Options-like 类型的值。

Playground link to code

【讨论】:

    【解决方案2】:

    我猜这就是你要找的东西

    class Arg<Name extends string> {
    
        name: Name
    
        public constructor(name: Name) {
            this.name = name
        }
    
    }
    
    class Int<Name extends string> extends Arg<Name> {}
    class Bool<Name extends string> extends Arg<Name> {}
    
    const createInt = <Name extends string>(name: Name) => new Int<Name>(name)
    const createBool = <Name extends string>(name: Name) => new Bool<Name>(name)
    
    type Unpacked<T> = T extends (infer U)[] ? U : T;
    
    type Options <T extends (Int<string> | Bool<string>)[], V extends Unpacked<T> = Unpacked<T>>= {
        args: T,
        execute: (args: Record<V['name'], V>) => void
    }
    
    const args = [
            createInt('age'),
            createBool('isStudent')
        ]
    
    const options: Options<typeof args> = {
        args,
        execute: args => {
            args.age
            args.age.length // Should be error, property 'length' does not exist on type 'number'.
            args.name // Should be error, property 'name' does not exist in 'args'.
            args.isStudent // Ok, boolean.
            
        }
    }
    

    playground

    【讨论】:

      猜你喜欢
      • 2021-11-16
      • 1970-01-01
      • 1970-01-01
      • 2019-05-12
      • 2021-10-24
      • 2021-11-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多