gpt4 book ai didi

javascript - 我需要如何更改这些 TypeScript mixin 类型定义才能允许定义允许类扩展特征的 mixin?

转载 作者:行者123 更新时间:2023-12-04 08:11:36 27 4
gpt4 key购买 nike

出于本问题的目的,将“mixin”视为 https://www.typescriptlang.org/docs/handbook/mixins.html 中所述的函数。 .在这种情况下,mixin 扩展了接收 mixin 的类。我正在尝试做一些不同的事情:启用“特征”,我在这里将其定义为可重用的类,这些类提供公共(public)和非公共(public)实例成员,这些成员可以被扩展特征的类继承和覆盖,这与 mixin 不同.
随后尝试解决方案,但类型不太正确,这就是我坚持的部分。请注意,这在 JavaScript 中非常有效,正如我编写的 npm 包 @northscaler/mutrait 所证明的那样。 .
我的问题是如何更改下面的类型定义以便代码编译和测试通过?
首先,这是模块,traitify.ts ,它试图成为这个的“库”(我知道它的类型定义不正确):

// in file traitify.ts

/**
* Type definition of a constructor.
*/
export type Constructor<T> = new(...args: any[]) => T;

/**
* A "trait" is a function that takes a superclass `S` and returns a new class `T extends S`.
*/
export type Trait<S extends Constructor<object>, T extends S> = (superclass: S) => T

/**
* Convenient function when defining a class that
* * extends a superclass, and
* * expresses one or more traits.
*/
export const superclass = <S extends Constructor<object>>(s?: S) => new TraitBuilder(s)

/**
* Convenient function to be used when a class
* * does not extend a superclass, and
* * expresses multiple traits.
*/
export const traits = <S extends Constructor<object>, T extends S>(t: Trait<S, T>) => superclass().with(t)

/**
* Convenient function to be used when defining a class that
* * does not extend a superclass, and
* * expresses exactly one trait.
*/
export const trait = <S extends Constructor<object>, T extends S>(t: Trait<S, T>) => traits(t).apply()

/**
* A convenient trait applier class that uses a builder pattern to apply traits.
*/
class TraitBuilder<S extends Constructor<object>> {
superclass: S;

constructor (superclass?: S) {
this.superclass = superclass || class {} as S // TODO: remove "as S" when figured out
}

/**
* Applies the trait to the current superclass then returns a new `TraitBuilder`.
* @param trait The trait that the current superclass should express.
*/
with <S extends Constructor<object>, T extends S>(trait: Trait<S, T>) {
// we have to return a new builder here because there's no way to take a collection of traits of differing types.
return new TraitBuilder(trait(this.superclass))
}

/**
* Return the class with all traits expressed.
*/
apply() {
return this.superclass || class {}
}
}

我希望能够定义一个 Taggable特征,在 Taggable.ts ,如下所示,其中 trait 定义了一个 protected _tag字段,并提供 tag 的默认实现属性(property):
// in file Taggable.ts

import { Constructor } from './traitify';

export interface ITaggable {
tag?: string;
}

export const Taggable = <S extends Constructor<object>>(superclass: S) =>
class extends superclass implements ITaggable {
_tag?: string; // TODO: make protected when https://github.com/microsoft/TypeScript/issues/36060 is fixed

get tag() {
return this._tag;
}

set tag(tag) {
this._doSetTag(this._testSetTag(tag));
}

constructor(...args: any[]) {
super(...args);
}

_testSetTag(tag?: string) { // TODO: make protected
return tag;
}

_doSetTag(tag?: string) { // TODO: make protected
this._tag = tag;
}
};

tag的默认实现属性是有意的,因为在这种模式中,我想允许 extend 的类trait 只覆盖它希望覆盖的那些 trait 成员。
在保持示例最小但彻底的同时,我必须包含一个更多的示例特征来说明当类为 extend 时的模式。多个特征,所以这里是 Nameable特质,与 Taggable 非常相似以上。
// in file Nameable.ts

import { Constructor } from './traitify';

export interface INameable {
name?: string;
}

export const Nameable = <S extends Constructor<object>>(superclass: S) =>
class extends superclass implements INameable {
_name?: string; // TODO: make protected when https://github.com/microsoft/TypeScript/issues/36060 is fixed

get name() {
return this._name;
}

set name(name) {
this._doSetName(this._testSetName(name));
}

constructor(...args: any[]) {
super(...args);
}

_testSetName(name?: string) { // TODO: make protected
return name;
}

_doSetName(name?: string) { // TODO: make protected
this._name = name;
}
};
现在,通过我们的 traitify库和两个特征,这里是我试图通过的测试,它说明了特征的使用者将如何使用它:
import { trait, superclass } from './traitify';

import test from 'ava';
import { Taggable } from './Taggable';
import { Nameable } from './Nameable';

test('express a single trait with no superclass', (t) => {
class Point extends trait(Taggable) {
constructor(public x: number, public y: number) {
super(...arguments);
this.x = x;
this.y = y;
}

_testSetTag(tag?: string) {
tag = super._testSetTag(tag);

if (!tag) throw new Error('no tag given');
else return tag.toLowerCase();
}
}

const point = new Point(10, 20);
point.tag = 'hello';

t.is(point.tag, 'hello');
t.throws(() => point.tag = '');
});

test('express a single trait and extend a superclass', (t) => {
class Base {
something: string = 'I am a base';
}

class Sub extends superclass(Base)
.with(Taggable).apply() {

constructor() {
super(...arguments);
}

_testSetTag(tag?: string): string | undefined {
tag = super._testSetTag(tag);

if (tag === 'throw') throw new Error('illegal tag value');
return tag;
}
}

const sub = new Sub();

t.assert(sub instanceof Sub);
t.assert(sub instanceof Base);

sub.tag = 'sub';

t.is(sub.tag, 'sub');
t.throws(() => sub.tag = 'throw');
});

test('express multiple traits and extend a superclass', (t) => {
class Animal {
}

class Person extends superclass(Animal)
.with(Nameable)
.with(Taggable).apply() {

constructor(...args: any[]) {
super(args);
}

_testSetName(name?: string) {
if (!name) throw new Error('no name given');
return name.trim();
}
}

const person = new Person();

t.assert(person instanceof Person);
t.assert(person instanceof Animal);

person.name = 'Felix';

t.is(person.name, 'Felix');
t.throws(() => person.name = null);
});

test('superclass expresses a trait, subclass expresses another trait but overrides method in superclass\'s trait', (t) => {
class Animal extends trait(Nameable) {
constructor(...args: any[]) {
super(args);
}

_testSetName(name?: string) {
if (!name) throw new Error('no name given');
if (name.toLowerCase().includes('animal')) throw new Error('name must include "animal"');
return name;
}
}

const animal = new Animal();
animal.name = 'an animal';

t.is(animal.name, 'an animal');
t.throws(() => animal.name = 'nothing');

class Person extends superclass(Animal)
.with(Taggable).apply() {

constructor(...args: any[]) {
super(args);
}

_testSetName(name?: string) {
if (!name) throw new Error('no name given');
if (name.toLowerCase().includes('person')) throw new Error('name must include "person"');
return name;
}
}

const person = new Person();
t.assert(person instanceof Person);
t.assert(person instanceof Animal);

person.name = 'a person';

t.is(person.name, 'a person');
t.throws(() => person.name = 'an animal');
t.throws(() => person.name = 'nothing');
});
我得到的编译器错误如下:
src/lib/traitify.spec.ts:84:10 - error TS2339: Property 'name' does not exist on type 'Person'.

84 person.name = 'Felix';
~~~~

src/lib/traitify.spec.ts:86:15 - error TS2339: Property 'name' does not exist on type 'Person'.

86 t.is(person.name, 'Felix');
~~~~

src/lib/traitify.spec.ts:87:25 - error TS2339: Property 'name' does not exist on type 'Person'.

87 t.throws(() => person.name = null);
~~~~

src/lib/traitify.spec.ts:127:10 - error TS2339: Property 'name' does not exist on type 'Person'.

127 person.name = 'a person';
~~~~

src/lib/traitify.spec.ts:129:15 - error TS2339: Property 'name' does not exist on type 'Person'.

129 t.is(person.name, 'a person');
~~~~

src/lib/traitify.spec.ts:130:25 - error TS2339: Property 'name' does not exist on type 'Person'.

130 t.throws(() => person.name = 'an animal');
~~~~

src/lib/traitify.spec.ts:131:25 - error TS2339: Property 'name' does not exist on type 'Person'.

131 t.throws(() => person.name = 'nothing');
~~~~

src/lib/traitify.ts:48:35 - error TS2345: Argument of type 'S' is not assignable to parameter of type 'S'.
'S' is assignable to the constraint of type 'S', but 'S' could be instantiated with a different subtype of constraint 'Constructor<object>'.
Type 'Constructor<object>' is not assignable to type 'S'.
'Constructor<object>' is assignable to the constraint of type 'S', but 'S' could be instantiated with a different subtype of constraint 'Constructor<object>'.

48 return new TraitBuilder(trait(this.superclass))
~~~~~~~~~~~~~~~


Found 8 errors.
注意:如果您想在 https://github.com/matthewadams/typescript-trait-test. 上使用它,可以使用 Git 存储库。要播放,请执行 git clone https://github.com/matthewadams/typescript-trait-test && cd typescript-trait-test && npm install && npm test .
NB:我觉得这真的是我所能提供的最小的展示我试图启用的模式。

最佳答案

好的,我终于在 TypeScript 中找到了一个不完美但可用的特征模式。
TL;DR:如果您只想查看演示该模式的代码,它是 here .查看 main & test文件夹。
一个 trait就本次讨论而言,它只是一个函数,它接受一个可选的父类(super class)并返回一个扩展给定父类(super class)并实现一个或多个特定于特征的接口(interface)的新类。这是众所周知的子类工厂模式的变体的组合 hereintersection types .
这是强制性的,尽管太少了,“​​你好,世界!”。
这是图书馆,如果你想称之为:

/**
* Type definition of a constructor.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T> = new (...args: any[]) => T

/**
* The empty class.
*/
export class Empty {}

/**
* A "trait" is a function that takes a superclass of type `Superclass` and returns a new class that is of type `Superclass & TraitInterface`.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export type Trait<Superclass extends Constructor<object>, TraitInterface> = (
superclass: Superclass
) => Constructor<Superclass & TraitInterface>
这是一个 Greetable特质,赋予 greeting属性(property)和打招呼的方法。请注意,它包含一个用于返回特征 implements 的公共(public)行为的接口(interface)。 .
// in file Greetable.ts

import { Constructor, Empty } from '../main/traitify'

/*
* Absolutely minimal demonstration of the trait pattern, in the spirit of "Hello, world!" demos.
* This is missing some common stuff because it's so minimal.
* See Greetable2 for a more realistic example.
*/

/**
* Public trait interface
*/
export interface Public {
greeting?: string

greet(greetee: string): string
}

/**
* The trait function.
*/
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/explicit-module-boundary-types
export const trait = <S extends Constructor<object>>(superclass?: S) =>
/**
* Class that implements the trait
*/
class Greetable extends (superclass || Empty) implements Public {
greeting?: string

/**
* Constructor that simply delegates to the super's constructor
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) {
super(...args)
}

greet(greetee: string): string {
return `${this.greeting}, ${greetee}!`
}
}
下面是如何编写一个类来表达特征。这是来自我的 mocha 单元测试。
// in file traitify.spec.ts

it('expresses the simplest possible "Hello, world!" trait', function () {
class HelloWorld extends Greetable.trait() {
constructor(greeting = 'Hello') {
super()
this.greeting = greeting
}
}

const greeter = new HelloWorld()

expect(greeter.greet('world')).to.equal('Hello, world!')
})
现在你已经看到了最简单的例子,让我分享一个更现实的“你好,世界!”这证明了一些更有用的东西。这是另一个 Greetable 特性,但它的行为可由表达类自定义。
// in file Greetable2.ts

import { Constructor, Empty } from '../main/traitify'

/**
* Public trait interface
*/
export interface Public {
greeting?: string

greet(greetee: string): string
}

/**
* Nonpublic trait interface
*/
export interface Implementation {
_greeting?: string

/**
* Validates, scrubs & returns given value
*/
_testSetGreeting(value?: string): string | undefined

/**
* Actually sets given value
*/
_doSetGreeting(value?: string): void
}

/**
* The trait function.
*/
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/explicit-module-boundary-types
export const trait = <S extends Constructor<object>>(superclass?: S) =>
/**
* Class that implements the trait
*/
class Greetable2 extends (superclass || Empty) implements Implementation {
_greeting?: string

/**
* Constructor that simply delegates to the super's constructor
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) {
super(...args)
}

get greeting() {
return this._greeting
}

set greeting(value: string | undefined) {
this._doSetGreeting(this._testSetGreeting(value))
}

greet(greetee: string): string {
return `${this.greeting}, ${greetee}!`
}

_testSetGreeting(value?: string) {
return value
}

_doSetGreeting(value?: string) {
this._greeting = value
}
}
这具有包括两个接口(interface)的关键特性,一个用于公共(public)行为,一个用于非公共(public)行为。 Public接口(interface)表示对表达特征的类的客户可见的行为。 Implementation interface 将实现的细节表示为一个接口(interface),而 Implementation extends Public .然后 trait 函数返回一个类 implements Implementation .
在这个实现中, _testSetGreeting方法验证、清理并返回设置的值,以及 _doSetGreeting实际设置支持属性 _greeting .
现在,表达 trait 的类可以覆盖它需要的任何东西以自定义行为。此示例覆盖 _testSetGreeting以确保发出问候语并 trim 问候语。
// in file traitify.spec.ts

it('expresses a more realistic "Hello, world!" trait', function () {
class HelloWorld2 extends Greetable2.trait() {
constructor(greeting = 'Hello') {
super()
this.greeting = greeting
}

/**
* Overrides default behavior
*/
_testSetGreeting(value?: string): string | undefined {
value = super._testSetGreeting(value)

if (!value) {
throw new Error('no greeting given')
}

return value.trim()
}
}

const greeter = new HelloWorld2()

expect(greeter.greet('world')).to.equal('Hello, world!')
expect(() => {
greeter.greeting = ''
}).to.throw()
})
repo中有更详尽的例子.
有时,TypeScript 仍然会出错,通常是当您表达多个 trait 时,这很常见。有一个方便的方法,呃,帮助 TypeScript 获得正确类型的表达特征的类:将表达类的构造函数的范围缩小到 protected并创建一个名为 new 的静态工厂方法使用 as 返回表达类的实例告诉 TypeScript 正确的类型是什么。这是一个例子。
  it('express multiple traits with no superclass', function () {
class Point2 extends Nameable.trait(Taggable.trait()) {
// required to overcome TypeScript compiler bug?
static new(x: number, y: number) {
return new this(x, y) as Point2 & Taggable.Public & Nameable.Public
}

protected constructor(public x: number, public y: number) {
super(x, y)
}

_testSetTag(tag?: string) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
tag = super._testSetTag(tag)

if (!tag) throw new Error('no tag given')
else return tag.toLowerCase()
}

_testSetName(name?: string) {
name = super._testSetName(name)

if (!name) throw new Error('no name given')
else return name.toLowerCase()
}
}

const point2 = Point2.new(10, 20)
point2.tag = 'hello'

expect(point2.tag).to.equal('hello')
expect(() => (point2.tag = '')).to.throw()
})
付出的代价相当小:你使用类似 const it = It.new() 的东西而不是 const it = new It() .这会让你走得更远,但你仍然需要撒一个 // @ts-ignore在这里和那里让 TypeScript 知道你知道你在做什么。
最后,还有一个限制。如果您希望一个类表达特征并扩展基类,那么特征将覆盖基类的方法(如果它们具有相同的名称)。然而,在实践中,这并不是一个严重的限制,因为一旦你采用了 trait 模式,它就有效地取代了 extends 的传统用法。 .

You should always author any reusable class-based code as a trait,then have your classes express the traits, not extend base classes.


摘要
我知道这并不完美,但它运作良好。我更愿意看到 TypeScript 提供 trait/ mixin & with作为此模式语法糖的关键字,很像 Dart's mixin & with Scala's traits ,除糖之外,所有范围和类型安全问题都将得到解决(以及 diamond problem 的解决方案)。
注意:已经有 JavaScript proposal for mixins .
这个模式花了我很长时间才识别出来,还是不对,但现在已经足够好了。这个特质模式有 worked great for me in JavaScript ,但我一直在尝试在 TypeScript 中显示相同的模式时遇到问题(而且我不是唯一一个)。
现在,还有其他解决方案试图解决这个问题,但我在这里提出的方案在简单性、可读性、可理解性和强大功能之间取得了很好的平衡。让我知道你的想法。

关于javascript - 我需要如何更改这些 TypeScript mixin 类型定义才能允许定义允许类扩展特征的 mixin?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65922936/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com