考慮以下代碼:
uj5u.com熱心網友回復:
在某種程度上,不可能完全防止物件型別中的額外鍵。TypeScript 的整個型別系統是圍繞結構型別構建的,其中物件型別只關心已知屬性而不禁止未知屬性。TypeScript 中的物件型別被認為是開放的和可擴展的,而不是封閉的或“精確的”(有關支持精確型別的功能請求,請參閱microsoft/TypeScript#12936)。
這意味著無論你如何撰寫f()的呼叫簽名,有人最終可能會傳遞未知屬性而不會做任何不合理的事情:
const val = { a: "ok", b: "fine", oooops: "???" };
const smth: { a: string, b: string } = val; // <-- this is accepted
f({ prop1: smth }) // and so is this
valto的賦值smth是可以接受的,因為它val具有 中的所有屬性{a: string, b: string}。存在額外oooops屬性的事實并不是錯誤。
在這里產生錯誤的檢查型別:
const notAllowed: { a: string, b: string } =
{ a: "ok", b: "fine", oooops: "???" }; // error!
// -------------------> ~~~~~~~~~~~~~
// Object literal may only specify known properties
稱為多余的屬性檢查,僅在編譯器看到額外屬性的知識將被編譯器丟棄的地方觸發。在 中notAllowed,您立即丟棄了該oooops屬性的知識,因此編譯器認為將其放在那里可能是錯誤的。這更像是一個 linter 規則而不是型別安全規則。
但是在const smth: {a: string, b: string } = val,val物件仍然單獨存在并且編譯器知道oooops. 在您的原始代碼中,S泛型型別引數將被推斷為其中包含oooops的內容。額外的屬性不會被丟棄,因此不會被視為錯誤。
所以這里的一個建議是只接受多余的屬性,確保 的實作在f()獲得它們時不會做壞事,不要擔心。
如果你真的想禁止額外的密鑰,你可以改變呼叫簽名f()來這樣做。我在這里的第一種方法適用于所有你關心的跟蹤是簡單的情況下鍵的obj引數:
type SmthValue =
{ args?: { [K: string]: () => string } } &
{ [K in SomeEnum]: string };
declare function f<K extends PropertyKey>(
obj: { [P in K]: SmthValue }
): { [P in K]: { [P in SomeEnum]: () => string } };
我們不跟蹤整個S型別引數,而是只跟蹤它的 keys K,以及我們剛剛使用的每個值SmthValue(與您的 相同Smth[string])。由于SmthValue是特定型別,如果您傳入帶有額外鍵的物件文字,該K型別將不知道它們,因此您將丟棄該資訊。這會觸發過多的屬性檢查警告:
f({
prop1: {
a: "abc",
b: "def",
},
prop2: {
args: {},
a: "zzz",
b: "xxx",
},
prop3: {
a: "ok",
b: "fine",
oooops: "???", // error!
// ~~~~~~~~~~
// Object literal may only specify known properties
}
})
但是你有一個回傳型別,f()它關心obj 屬性值和鍵的細節。所以這對你不起作用。
在需要保留的一般情況下S,我們可以撰寫一個型別函式,通過查找額外的屬性并將它們的值型別映射到 來模擬精確型別never。有關詳細資訊,請參閱microsoft/TypeScript#12936 上的此評論。這是我如何寫的:
declare function f<S extends Record<keyof S, SmthValue>>(obj: ProhibitExtraKeys<S>): {
[K in keyof S]: { [P in SomeEnum]: S[K] extends { args: any } ? () => string : string } }
當您呼叫時f(something),編譯器將推斷S為typeof something,然后根據 進行檢查ProhibitExtraKeys<S>。如果S可分配給ProhibitExtraKeys<S>那么一切都很好。如果S是沒有分配給ProhibitExtraKeys<S>然后你會得到一個編譯器警告。
所以這是ProhibitExtraKeys<S>:
type ProhibitExtraKeys<S> = {
[K in keyof S]: {
[P in keyof S[K]]: P extends (keyof SmthValue) | `${keyof SmthValue}` ? S[K][P] : never
}
};
It walks through each property key K of S (which is allowed to be anything) and then for each subproperty key P of S[K], it maps the property value to either itself, or never, depending on whether that key P is expected or not.
The expected keys are (keyof SmthValue) | `${keyof SmthValue}`. If you didn't have an enum and were using "a" | "b" instead, I'd just use keyof SmthValue, which would evaluate to "args" | "a" | "b" and we'd be done. But keyof SmthValue is "args" | SomeEnum. And unfortunately, enum values are a bit weird. This is an error:
const x: SomeEnum = "a" // error!
// Type '"a"' is not assignable to type 'SomeEnum'
And so unless you want to prohibit P from being "a" or "b", you need to get the string values of SomeEnum. Which you can do with template literal types:
type AcceptableKeys = (keyof SmthValue) | `${keyof SmthValue}`
// type AcceptableKeys = SomeEnum | "args" | "a" | "b"
現在,我們將接受"args"或者"a" | "b"甚至SomeEnum。讓我們看看它的作業原理:
const t = f({
prop1: {
a: "abc",
b: "def",
},
prop2: {
args: {},
[SomeEnum.a]: "zzz", // <-- okay too
b: "xxx",
},
prop3: {
a: "ok",
b: "fine",
oooops: "???", // error!
//~~~~
//Type 'string' is not assignable to type 'never'
}
})
你去吧。我認為輸入和輸出是你想要的。
Playground 鏈接到代碼
uj5u.com熱心網友回復:
為什么你寫成f一個接受任何型別的泛型函式extends Smth?只需將其撰寫為常規函式或洗掉extends約束即可:
const enum SomeEnum {
a = "a",
b = "b",
}
type Smth = {
[key: string]: {
args?: { [key: string]: () => string }
} & {
[key in SomeEnum]: string
}
}
function f(obj: Smth): { [key in keyof Smth]: { [key in SomeEnum]: () => string } } {
return null!
}
const t = f({
prop1: {
a: "abc",
b: "def",
},
prop2: {
args: {},
a: "zzz",
b: "xxx",
},
prop3: {
a: "ok",
b: "fine",
oooops: "???", // Should be an error
}
})
打字稿游樂場
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/375633.html
