繁体   English   中英

JavaScript/TypeScript 类实例之间的唯一属性值

[英]Unique property values between instances of classes in JavaScript/TypeScript

给定一个接口IThing

interface IThing {
    name: string;
}

有没有办法确保实现IThing的类使用name的唯一值?

IE

class ThingOne implements IThing {
    name = "Name_One"
}

class ThingTwo implements IThing {
    name = "Name_Two"
}

应该允许,而

class ThingOne implements IThing {
    name = "Name_One"
}

class ThingTwo implements IThing {
    name = "Name_One"
}

应导致 TypeScript 错误。

属性值是可读和手写的,因为它们描述了 object 本身,因此无法生成它们。

我只看到使用 Set 来跟踪唯一名称的决定

 let usedNames = new Set<string>(); class ThingOne implements IThing { name: string; constructor(name: string) { if (usedNames.has(name)) { throw new Error(`Name "${name}" has already been used.`); } usedNames.add(name); this.name = name; } }

TypeScript 实际上没有任何内置方法来确保不同类中属性的唯一性,它甚至没有跟踪哪些类以开发人员可以查询的方式扩展/实现其他类/接口( 结构类型系统意味着一个 class 可以实现一个接口,即使你没有这样声明它,所以实现一个接口的东西的集合可能是无限的,这与标称类型系统不同)。

因此,如果您想要这样的保证,您将需要显着重构您的代码以将其他一些 TypeScript 功能投入服务,作为一种解决方法。


一种可能的方法是将您的类存储在注册表 object 中,并通过调用断言 function将每个 class 添加到 object通过缩小。 也许像这样:

interface IThing {
    name: string;
    prop: string;
    generate(): string;
}

type ValidateUniqueThing<TS extends IThing, T extends IThing> = {
    [K in "name" | "prop"]:
    string extends T[K] ? [Error, "Make", K, "readonly PLEASE"] :
    T[K] extends TS[K] ? [Error, "Conflicting ", K, "Property with class named ",
        Extract<TS, { [P in K]: T[K] }>["name"]] :
    T[K]
}

function registerThing<R extends Record<keyof R, IThing>, T extends IThing>(
    registry: { [K in keyof R]: new () => R[K] },
    name: T['name'],
    newThing: new () => (
        T extends ValidateUniqueThing<R[keyof R], T> ? T : ValidateUniqueThing<R[keyof R], T>
    )
): asserts registry is Extract<
    { [K in keyof R | T['name']]: new () => K extends keyof R ? R[K] : T },
    typeof registry
> {
    Object.assign(registry, { [name]: newThing });
}

因此,实用程序类型ValidateUniqueThing<TS, T>采用现有Thing类型TS联合,以及新添加的候选类型T 如果T是可接受的加法,则ValidateUniqueThing<TS, T>的计算结果仅为T 否则,违反规则的T属性将映射到不兼容的类型,其显示信息有望为开发人员提供一些有关如何解决问题的信息。

这里有两种主要可能的违规行为:

  • 如果该属性是string类型而不是文字类型,那么编译器不知道该值实际是什么,也无法保证唯一性。 这通常发生在您使用字符串文字初始化属性而未将属性标记为readonly时,因此不兼容的类型提到readonly (但您可以将其更改为任何您想要的)。

  • 如果该属性已存在于其他已注册类中相同属性的现有文字类型的联合中,则编译器已发现唯一性违规。 不兼容类型试图找出哪个 class 已使用该名称,使用Extract<T, U>实用程序类型

registerThing() function 将registry object 作为其第一个参数,将新的 class 实例的name属性作为其第二个参数,将要注册的新事物newThing作为其第三个参数。 它在R中是通用的,class name s 到它们的实例类型的映射, T是新 class 的实例类型Rname的类型是到registryT的直接映射和索引 newThing的类型取决于ValidateUniqueThing<R[keyof R], T> (其中R[keyof R]是已注册IThing实例类型的联合),如果newThing有效,则类型与其匹配(它只是变成new () => T ),但如果它无效,则类型不匹配(它变成new () => X ,其中X是上面ValidateUniqueThing中的不兼容类型),你会收到一条错误消息.

因为registerThing()是一个断言 function,它的返回类型asserts registry is...将导致在调用 function 之后注册表对象的类型变窄。 它缩小到的类型只是现有注册表 object 类型,添加了一个与newThing参数相对应的新属性。


好的,让我们看看实际效果。

首先我们创建一个空注册表 object:

const _Things = {};
// const _Things: {}

我在该名称前加上_ ,因为一旦完成,我们会将其复制到一个新的const中,以确保它可以在任何地方使用。 让我们注册一个 class:

registerThing(_Things, "ThingOne", class ThingOne {
    readonly name = "ThingOne";
    readonly prop = "Prop";
    generate() {
        return this.name + " " + this.prop
    }
}); // okay

_Things;
/* const _Things: {
    ThingOne: new () => ThingOne;
} */

成功了,现在我们可以看到_Things的类型已更改以反映添加。 让我们再做一次:

registerThing(_Things, "ThingTwo", class ThingTwo {
    readonly name = "ThingTwo";
    readonly prop = "PropTwo";
    generate() {
        return this.name + " " + this.prop
    }
}); // okay

_Things;
/* const _Things: {
    ThingOne: new () => ThingOne;
    ThingTwo: new () => ThingTwo;
} */

看起来还是不错的。 好的,现在让我们犯一个错误:

registerThing(_Things, "ThingThree", class ThingThree { // error!
    // '[Error, "Conflicting ", "prop", "Property with class named ", "ThingTwo"]
    readonly name = "ThingThree";
    readonly prop = "PropTwo";
    generate() {
        return "wha"
    }
});

呃哦,我们有一个错误。 如果您眯着眼睛看错误消息,它会说propThingTwo冲突。 让我们解决这个问题:

registerThing(_Things, "ThingThree", class ThingThree {
    readonly name = "ThingThree";
    readonly prop = "PropThree";
    generate() {
        return "wha"
    }
}); // okay

让我们犯另一个错误:

registerThing(_Things, "ThingFour", class ThingFour { // error!
    // [Error, "Make", "name", "readonly PLEASE"]
    name = "ThingFour";
    readonly prop = "PropFour";
    generate() {
        return ""
    }
});

呃哦,我们有一个错误。 眯着眼睛发现name不是readonly的,因此是string类型而不是"ThingFour" 让我们解决这个问题:

registerThing(_Things, "ThingFour", class ThingFour {    
    readonly name = "ThingFour";
    readonly prop = "PropFour";
    generate() {
        return ""
    }
}); // okay

伟大的。 现在我们将完成的_Things复制到Things

const Things = _Things;
/*
const Things: {
    ThingOne: new () => ThingOne;
    ThingTwo: new () => ThingTwo;
    ThingThree: new () => ThingThree;
    ThingFour: new () => ThingFour;
}*/

它有我们想要的所有道具。 要使用这些类,我们只需索引到Things

function foo() {
    const thing2 = new Things.ThingTwo();
    // const thing2: ThingTwo
    console.log(thing2.generate())
}

所以,这行得通! 我将_Things复制到Things的原因是,与所有控制流分析效果一样,断言函数中完成的缩小不会跨越 function 边界持续存在。 所以在foo()内部,您会发现_Things看起来完全是空的:

function foo() {
    const thing2 = new _Things.ThingTwo(); // error!
    // Property 'ThingTwo' does not exist on type '{}'.
}

这很不幸,但是为了防止编译器必须做很多额外的工作来尝试找出关于何时配置_Thingsfoo()的调用时间,这是不可行的(请参阅microsoft/TypeScript#9998了解关于一般问题的讨论)。 很容易到达编译器知道窄化类型有效的地方,然后复制到无论在何处使用其类型都相同的const


所以,万岁,整个事情都有效。 它很复杂,也许太复杂了,对你没有用。 可能其他一些方法更符合您的喜好,但我能想到的所有方法(包括这种方法)都会涉及某种循环、冗余、丑陋或“非局部”错误。

我的意思是您将捕获唯一性违规,但错误消息不会靠近发生违规的位置。 例如,如果您构建了某种手动维护的所有类的联合(这是多余的),然后编写了一个类型来探测它是否违反了唯一性,则错误将在该类型附近的某处发生,并且不一定接近 class问题。

所以我认为这最终将归结为品味和对用例的适用性问题,通常情况下都是变通办法。

游乐场代码链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM