Javascript 我需要如何更改这些TypeScript混合类型定义,以便允许定义允许类扩展特性的混合类型? 对于这个问题的目的,考虑一个“MIXIN”是一个函数。在这种情况下,mixin扩展了接收mixin的类。我试图做一些不同的事情:启用“traits”,我在这里定义为可重用类,这些类提供公共和非公共实例成员,这些成员可以被扩展trait的类继承和重写,而不像mixin
随后尝试了一个解决方案,但打字不太正确,这就是我一直坚持的部分。请注意,这在JavaScript中非常有效,我编写的npm包就证明了这一点 我的问题是如何更改下面的类型定义,以便代码编译和测试通过 首先,这里有一个模块,Javascript 我需要如何更改这些TypeScript混合类型定义,以便允许定义允许类扩展特性的混合类型? 对于这个问题的目的,考虑一个“MIXIN”是一个函数。在这种情况下,mixin扩展了接收mixin的类。我试图做一些不同的事情:启用“traits”,我在这里定义为可重用类,这些类提供公共和非公共实例成员,这些成员可以被扩展trait的类继承和重写,而不像mixin,javascript,typescript,traits,multiple-inheritance,mixins,Javascript,Typescript,Traits,Multiple Inheritance,Mixins,随后尝试了一个解决方案,但打字不太正确,这就是我一直坚持的部分。请注意,这在JavaScript中非常有效,我编写的npm包就证明了这一点 我的问题是如何更改下面的类型定义,以便代码编译和测试通过 首先,这里有一个模块,traitify.ts,它试图成为这个模块的“库”(我知道它的类型定义不正确): 标记属性的默认实现是有意的,因为在这个模式中,我希望允许扩展trait的类只覆盖它希望覆盖的trait的那些成员 在保持示例最少但全面的同时,我必须再包含一个示例特征,以说明当类扩展多个特征时的模式
traitify.ts
,它试图成为这个模块的“库”(我知道它的类型定义不正确):
标记
属性的默认实现是有意的,因为在这个模式中,我希望允许扩展
trait的类只覆盖它希望覆盖的trait的那些成员
在保持示例最少但全面的同时,我必须再包含一个示例特征,以说明当类扩展多个特征时的模式,因此这里有一个可命名的特征,非常类似于上面的可标记的特征
// 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;
}
};
我得到的编译器错误如下:
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.
src/lib/traitify.spec.ts:84:10-错误TS2339:类型“Person”上不存在属性“name”。
84 person.name='Felix';
~~~~
src/lib/traitify.spec.ts:86:15-错误TS2339:类型“Person”上不存在属性“name”。
86 t.is(人名“Felix”);
~~~~
src/lib/traitify.spec.ts:87:25-错误TS2339:类型“Person”上不存在属性“name”。
87 t.throws(()=>person.name=null);
~~~~
src/lib/traitify.spec.ts:127:10-错误TS2339:类型“Person”上不存在属性“name”。
127 person.name='a person';
~~~~
src/lib/traitify.spec.ts:129:15-错误TS2339:类型“Person”上不存在属性“name”。
129 t.is(person.name,'a person');
~~~~
src/lib/traitify.spec.ts:130:25-错误TS2339:类型“Person”上不存在属性“name”。
130吨(()=>person.name='animal');
~~~~
src/lib/traitify.spec.ts:131:25-错误TS2339:类型“Person”上不存在属性“name”。
131 t.throws(()=>person.name='nothing');
~~~~
src/lib/traitify.ts:48:35-错误TS2345:类型为“S”的参数不能分配给类型为“S”的参数。
“S”可分配给“S”类型的约束,但“S”可以用约束“构造函数”的不同子类型实例化。
类型“构造函数”不可分配给类型“S”。
“Constructor”可分配给“S”类型的约束,但“S”可以用约束“Constructor”的不同子类型实例化。
48返回新的TraitBuilder(trait(this.superclass))
~~~~~~~~~~~~~~~
发现8个错误。
注意:有一个Git repo可用于此,如果您想在播放时使用它,请执行Git clonehttps://github.com/matthewadams/typescript-trait-test &&cd类型脚本特性测试和npm安装和npm测试
注:我觉得这真的是我所能提供的最低限度,以演示我试图启用的模式。好的,我终于在TypeScript中找到了一个不完美但可用的特质模式 TL;DR:如果你只是想看看演示模式的代码,那就太简单了。查看
主
和测试
文件夹
在本讨论中,trait
只是一个函数,它接受一个可选的超类,并返回一个扩展给定超类并实现一个或多个特定于trait的接口的新类。这是已知的子类工厂模式的变体的组合
这里有一个强制性的,虽然太少了,“你好,世界!”
这就是图书馆,如果你想叫它:
/**
* 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>
下面是如何编写一个类来表达这个特性。这是我的摩卡单元测试
// 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!')
})
现在您已经看到了最简单的示例,让我来分享一个更现实的“Hello,world!”,它演示了一些更有用的东西。这是另一个Greetable特性,但其行为可由expressing类自定义
// 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
}
}
在本文中有更详尽的例子
有时候,TypeScript仍然会出错,通常是当你表达了一个以上的特征时,这是很常见的。有一个方便的方法,呃,帮助,TypeScript获取表达traits的类的正确类型:将表达类的构造函数的范围缩小为受保护的
,并创建一个名为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知道你知道自己在做什么
最后,有一个限制。如果您想要一个类来表达traits并扩展基类,那么如果traits具有相同的名称,那么它们将覆盖基类的方法。然而,在实践中,这并不是一个严重的限制,因为一旦你采用了特质模式,它就有效地取代了传统的extends
您应该始终编写任何可重用的基于类的代码作为特征,
那就让你的班级来表达你的特点,不是吗
// 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}!`
}
}
// 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!')
})
// 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
}
}
// 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()
})
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()
})