簡體   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