程序问答   发布时间:2022-06-02  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。

如何解决使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的?

开发过程中遇到使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的的问题如何解决?下面主要结合日常开发的经验,给出你关于使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的的解决方法建议,希望对你解决使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的有所启发或帮助;

假设我有一个带有构造函数的类 Base,它需要一个对象参数至少有一个 version 键。 Base 类还有一个静态 .defaults() 方法,它可以为它返回的新构造函数的任何选项设置默认值。

在代码中,这是我想要的

const test = new Base({
  // `version` should be typed as required for the `Base` constructor
  version: "1.2.3"
})
const MyBaseWithDefaults = Base.defaults({
  // `version` should be typed as optional for `.defaults()`
  foo: "bar"
})
const MyBaseWithVersion = Base.defaults({
  version: "1.2.3",foo: "bar"
})
const testWithDefaults = new MyBaseWithVersion({
  // `version` should not be required to be set at all
})

// should be both typed as string
testWithDefaults.options.version
testWithDefaults.options.foo

额外问题:如果因为 options 是通过 version 设置而不需要任何键,是否可以将构造函数 .defaults() 参数设为可选?

这是我到目前为止的代码:

interface Options {
  version: string;
  [key: string]: unkNown;
}

type Constructor<T> = new (...args: any[]) => T;

class Base<toptions extends Options = Options> {
  static defaults<
    TDefaults extends Options,S extends Constructor<Base<TDefaults>>
  >(
    this: S,defaults: Partial<TDefaults>
  ): {
    new (...args: any[]): {
      options: TDefaults;
    };
  } & S {
    return class extends this {
      constructor(...args: any[]) {
        super(Object.assign({},defaults,args[0] || {}));
      }
    };
  }

  constructor(options: toptions) {
    this.options = options;
  };
  
  options: toptions;
}

TypeScript playground

7 月 5 日更新

我应该提到级联默认值应该有效:Base.defaults({ one: "" }).defaults({ two: "" })

解决方法

让我们来看看这些要求,并制定一个关于如何实施它们的粗略计划。

  1. 可以设置默认值的静态.defaults()方法

    这里的想法是在原始构造函数(即子构造函数)“上方”创建一个结构。此结构将采用默认值和其余值,将它们组合成一个对象,并将其提供给原始构造函数。实际上,您在这方面非常接近。这个设置的类型肯定会包括泛型,但如果你熟悉泛型,这应该不是问题。

  2. 如果不需要任何键,则将构造函数 options 参数设为可选

    这部分问题实际上比您想象的要棘手。要实现它,我们必须使用:

    • 事实上,keyof 运算符在应用于没有属性的对象 ({}) 时会产生 never(“空对象永远没有属性,因此也没有键”) ;
    • TypeScript 能够以元组的形式在函数参数列表中移动,这将有助于根据给定的条件(在我们的例子中,它是“对象为空”)为构造函数分配不同的参数;立>
  3. 级联默认值:Base.defaults({ one: "" }).defaults({ two: "" })

    因为 .defaults() 的结果是一个子类(见上文),它必须具有其父类的所有静态成员,包括 .defaults() 本身。所以,从纯 JavaScript 的角度来看,没有什么新的实现,它应该已经可以工作了。

    然而,在 TypeScript 中,我们遇到了一个大问题。 .defaults() 方法必须能够访问当前类的默认值,以便为新的默认值生成类型,这些类型由旧对象和新对象组合而成。例如,给定标题中的情况,为了得到 { one: string } & { two: string },我们直接从参数推断 { two: string }(新默认值),并从其他地方推断 { one: string }(旧默认值)。最好的地方是类的类型参数(例如,class Base<Defaults extends Options>),但这里是交易:static members cannot reference class type parameters。

    有 a workaround for this,但是,它需要一些半合理的假设和一点点放弃 DRY。最重要的是,您不能再以声明方式定义类,您必须命令式(也称为“动态”)创建继承链的第一个“最顶层”成员(如 const Class = createClass();),我个人觉得相当不幸(尽管它工作得很好)。

说了这么多,下面是结果(还有一个 playground;随意折叠/删除 <TRIAGE> 部分):

type WithOptional<
    OriginalObject extends object,OptionalKey extends keyof OriginalObject = never,> = Omit<OriginalObject,OptionalKey> & Partial<Pick<OriginalObject,OptionalKey>>;

type KeyOfByValue<Obj extends object,Value> = {
    [Key in keyof Obj]: Obj[Key] extends Value ? Key : never;
}[keyof Obj];

type RequiredKey<Obj extends object> =
    Exclude<KeyOfByValue<Obj,Exclude<Obj[keyof Obj],undefined>>,undefined>;

type OptionalParamIfEmpty<Obj extends object> =
    RequiredKey<Obj> extends never ? [ Obj? ] : [ Obj ];

function createClass<
    Options extends object,OptionalKey extends keyof Options = never,>(
    defaults?: Pick<Options,OptionalKey>,Parent: new(options: Options) => object = Object
) {
    return class Class extends Parent {
        static defaults<
            OptionalKey2 extends keyof Options,>(
            additionalDefaults: Pick<Options,OptionalKey2>,) {
            const newDefaults = { ...defaults,...additionalDefaults } as Options;

            return createClass<Options,OptionalKey | OptionalKey2>(newDefaults,this);
        }

        public options: Options;

        constructor(
            ...[explicit]: OptionalParamIfEmpty<WithOptional<Options,OptionalKey>>
        ) {
            const options = { ...defaults,...explicit } as Options;

            super(options);

            this.options = options;
        }
    }
}

分解:

  • createClass() 应该只被显式调用以创建继承链中的第一个类(后续子类通过 .defaults() 调用创建)。

  • createClass() 需要(全部 - 可选):

    • options 属性的类型定义;
    • 要预填充的 options 摘录(defaults 对象,函数的第一个值参数);
    • 对父类的引用(第二个值参数),默认设置为 Object(所有对象的公共父类)。
  • 应该明确提供 Options 中的 createClass() 类型参数。

  • OptionalKey 中的 createClass() 类型参数是根据提供的 defaults 对象的类型自动推断的。

  • createClass() 返回一个具有更新类型 constructor() 的类,即 defaults 中已经存在的属性在 explicit 中不再需要。>

  • 如果所有 options 属性都是可选的,则 explicit 参数本身变为可选。

  • 由于返回类的整个定义都放在函数内部,所以它的 .defaults() 方法可以通过闭包直接访问上面的 defaults 对象。这允许该方法只需要额外的默认值;然后将两组默认值合并到一个对象中,并与当前类的定义一起传递给 createClass(defaults,Parent) 以创建一个具有预填充默认值的新子类。

  • 由于返回的类需要在构造函数的某处调用 super(),为了一致性,父类的构造函数被强制采用 options: Options 作为它的第一个参数。然而,构造函数完全有可能忽略这个参数;这就是为什么在 super() 调用之后,options 属性的值无论如何都会被显式设置。

,
interface Options {
  version: string;
  [key: string]: unknown;
}

type CalssType = abstract new (...args: any) => any;

// obtain parameters of constructor
type Params = ConstructorParameters<typeof Base>

// obtain instance type
type Instance = InstanceType<typeof Base>

// make first element of tuple Partial
type FstPartial<Tuple extends any[]> = Tuple extends [infer Fst,...infer Tail] ? [Partial<Fst>,...Tail] : never

// clone constructor type and replace first argument in rest parameters with partial
type GetConstructor<T extends CalssType> = new (...args: FstPartial<ConstructorParameters<T>>) => InstanceType<T>

type Constructor<T> = new (...args: any[]) => T;

class Base<TOptions extends Options = Options> {
  static defaults<
    TDefaults extends Options,S extends Constructor<Base<TDefaults>>
  >(
    this: S,defaults: Partial<TDefaults>
  ): {
    new(...args: any[]): {
      options: TDefaults;
    };
  } & GetConstructor<typeof Base> { // <--- change is here
    return class extends this {
      constructor(...args: any[]) {
        super(Object.assign({},defaults,args[0] || {}));
      }
    };
  }

  constructor(options: TOptions) {
    this.options = options;
  };

  options: TOptions;
}

const test = new Base({
  // `version` should be typed as required for the `Base` constructor
  version: "1.2.3"
})
const MyBaseWithDefaults = Base.defaults({
  // `version` should be typed as optional for `.defaults()`
  foo: "bar"
})
const MyBaseWithVersion = Base.defaults({
  version: "1.2.3",foo: "bar"
})
const testWithDefaults = new MyBaseWithVersion({}) // ok

// should be both typed as string
testWithDefaults.options.version
testWithDefaults.options.foo

Playground

,

在 Josh Goldberg 的帮助下,我们得出结论,无法使用当今的 TypeScript 键入无限可链接的 Base.defaults().defaults()... API。

我们改用的是实现最多 3 个 .defaults() 调用的链接,这仍然会在实例上正确设置 .options 属性。

这是我们最终实现的完整类型声明文件。它有点复杂,因为它还包含 .plugin()/.plugins API,为了简单起见,我从最初的问题中省略了该 API。

export declare namespace Base {
  interface Options {
    version: string;
    [key: string]: unknown;
  }
}

declare type ApiExtension = {
  [key: string]: unknown;
};
declare type Plugin = (
  instance: Base,options: Base.Options
) => ApiExtension | void;

declare type Constructor<T> = new (...args: any[]) => T;
/**
 * @author https://stackoverflow.com/users/2887218/jcalz
 * @see https://stackoverflow.com/a/50375286/10325032
 */
declare type UnionToIntersection<Union> = (
  Union extends any ? (argument: Union) => void : never
) extends (argument: infer Intersection) => void
  ? Intersection
  : never;
declare type AnyFunction = (...args: any) => any;
declare type ReturnTypeOf<T extends AnyFunction | AnyFunction[]> =
  T extends AnyFunction
    ? ReturnType<T>
    : T extends AnyFunction[]
    ? UnionToIntersection<Exclude<ReturnType<T[number]>,void>>
    : never;

type ClassWithPlugins = Constructor<any> & {
  plugins: any[];
};

type ConstructorRequiringVersion<Class extends ClassWithPlugins,PredefinedOptions> = {
  defaultOptions: PredefinedOptions;
} & (PredefinedOptions extends { version: string }
  ? {
      new <NowProvided>(options?: NowProvided): Class & {
        options: NowProvided & PredefinedOptions;
      };
    }
  : {
      new <NowProvided>(options: Base.Options & NowProvided): Class & {
        options: NowProvided & PredefinedOptions;
      };
    });

export declare class Base<TOptions extends Base.Options = Base.Options> {
  static plugins: Plugin[];

  /**
   * Pass one or multiple plugin functions to extend the `Base` class.
   * The instance of the new class will be extended with any keys returned by the passed plugins.
   * Pass one argument per plugin function.
   *
   * ```js
   * export function helloWorld() {
   *   return {
   *     helloWorld () {
   *       console.log('Hello world!');
   *     }
   *   };
   * }
   *
   * const MyBase = Base.plugin(helloWorld);
   * const base = new MyBase();
   * base.helloWorld(); // `base.helloWorld` is typed as function
   * ```
   */
  static plugin<
    Class extends ClassWithPlugins,Plugins extends [Plugin,...Plugin[]],>(
    this: Class,...plugins: Plugins,): Class & {
    plugins: [...Class['plugins'],...Plugins];
  } & Constructor<UnionToIntersection<ReturnTypeOf<Plugins>>>;

  /**
   * Set defaults for the constructor
   *
   * ```js
   * const MyBase = Base.defaults({ version: '1.0.0',otherDefault: 'value' });
   * const base = new MyBase({ option: 'value' }); // `version` option is not required
   * base.options // typed as `{ version: string,otherDefault: string,option: string }`
   * ```
   * @remarks
   * Ideally,we would want to make this infinitely recursive: allowing any number of
   * .defaults({ ... }).defaults({ ... }).defaults({ ... }).defaults({ ... })...
   * However,we don't see a clean way in today's TypeScript syntax to do so.
   * We instead artificially limit accurate type inference to just three levels,* since real users are not likely to go past that.
   * @see https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/57
   */
  static defaults<
    PredefinedOptionsOne,ClassOne extends Constructor<Base<Base.Options & PredefinedOptionsOne>> & ClassWithPlugins
  >(
    this: ClassOne,defaults: PredefinedOptionsOne
  ): ConstructorRequiringVersion<ClassOne,PredefinedOptionsOne> & {
    defaults<ClassTwo,PredefinedOptionsTwo>(
      this: ClassTwo,defaults: PredefinedOptionsTwo
    ): ConstructorRequiringVersion<
      ClassOne & ClassTwo,PredefinedOptionsOne & PredefinedOptionsTwo
    > & {
      defaults<ClassThree,PredefinedOptionsThree>(
        this: ClassThree,defaults: PredefinedOptionsThree
      ): ConstructorRequiringVersion<
        ClassOne & ClassTwo & ClassThree,PredefinedOptionsOne & PredefinedOptionsTwo & PredefinedOptionsThree
      > & ClassOne & ClassTwo & ClassThree;
    } & ClassOne & ClassTwo;
  } & ClassOne;

  static defaultOptions: {};

  /**
   * options passed to the constructor as constructor defaults
   */
  options: TOptions;

  constructor(options: TOptions);
}
export {};

这是将该功能添加到 javascript-plugin-architecture-with-typescript-definitions 模块的拉取请求。

https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/59

大佬总结

以上是大佬教程为你收集整理的使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的全部内容,希望文章能够帮你解决使用 MyClass.defaults(options) 设置必需的构造函数选项时,如何使它们成为可选的所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。
标签: