網站搜尋

泛型 - TypeScript Partial<T> 類型不包含 undefined


如何創建一個 kinda-Partial<T> 類型,而不允許 undefined 值?

這是一個例子:

interface MyType {
  foo: string
  bar?: number
}

const merge = (value1: MyType, value2: KindaPartial<MyType>): MyType => {
  return {...value1, ...value2};
}



const value = {
  foo: 'foo',
  bar: 42
}

merge(value, {});                          // should work
merge(value, { foo: 'bar' });              // should work
merge(value, { bar: undefined });          // should work
merge(value, { bar: 666 });                // should work
merge(value, { foo: '', bar: undefined }); // should work
merge(value, { foo: '', bar: 666 });       // should work

// now the problematic case:
merge(value, { foo: undefined }); // this should throw an error
                                  // because MyType["foo"] is of type string

我正在尋找的類型應該是:

  • 只接受存在於泛型類型上的鍵(就像普通的 Partial<T> 一樣)
  • 接受泛型類型鍵的子集
  • 但是 如果泛型類型不接受該鍵的 undefined,則不要接受 undefined

這可能嗎?

編輯:我也在 TypeScript 倉庫創建了一個問題,因為這很奇怪,應該在某個時候拋出錯誤:https://github.com/Microsoft/TypeScript/issues/29701

答案#1

TS 4.4 更新:

TS4.4 將有一個 --exactOptionalPropertyTypes 編譯器標誌,可以讓你直接使用 Partial 得到你想要的行為,只要你有意地在希望允許的地方加入 undefined:

interface MyType {
  foo: string
  bar?: number | undefined // <-- you want this
}

const merge = (value1: MyType, value2: Partial<MyType>): MyType => {
  return { ...value1, ...value2 };
}


const value = {
  foo: 'foo',
  bar: 42
}

merge(value, {});                          // okay
merge(value, { foo: 'bar' });              // okay
merge(value, { bar: undefined });          // okay
merge(value, { bar: 666 });                // okay
merge(value, { foo: '', bar: undefined }); // okay
merge(value, { foo: '', bar: 666 });       // okay

// now the problematic case:
merge(value, { foo: undefined }); // error!
// ----------> ~~~
// Type 'undefined' is not assignable to type 'string'

程式碼的 Playground 連結 <-- 注意,目前你需要在 TS 設定標籤中自行開啟 --exactOptionalPropertyTypes;不知為何該網址已壞

——--

TS4.4 之前的答案:

這是一個已知的限制(參見 microsoft/TypeScript#13195),TypeScript 無法正確區分物件屬性(以及函數參數)中 缺失的 與 存在但為 undefined 的屬性。Partial<T> 允許 undefined 屬性就是這一現象的結果。正確的做法是等待這個問題被解決(如果你在 GitHub 上前往該 issue 並點讚 👍 或留下帶有令人信服用例的評論,這個問題被解決的可能性可能會更大)。

如果你不想等待,你也許可以使用以下這種簡陋的方法來獲得類似的行為:

type VerifyKindaPartial<T, KP> = 
  Partial<T> & {[K in keyof KP]-?: K extends keyof T ? T[K] : never}; 

const merge = <KP>(value1: MyType, value2: KP & VerifyKindaPartial<MyType, KP>): MyType => {
  return { ...value1, ...value2 };
}

所以你不能直接寫 KindaPartial<T>。但是你可以寫一個類型 VerifyKindaPartial<T, KP>,它接受一個類型 T 和一個你想要檢查是否符合你想要的 KindaPartial<T> 的 候選 類型 KP。如果候選符合,則返回與 KP 匹配的東西。否則,它返回不匹配的東西。

然後,你將 merge() 變成一個泛型函數,該函數從傳入 value2 的值的類型推斷 KP。如果 KP & VerifyKindaPartial<MyType, KP> 匹配 KP(這意味著 KP 匹配 KindaPartial<MyType>),那麼程式碼將會編譯。否則,如果 KP & VerifyKindaPartial<MyType, KP> 不匹配 KP(這意味著 KP 不匹配 KindaPartial<MyType>),那麼就會出現錯誤。(不過,錯誤可能不太直觀)。

讓我們看看:

merge(value, {});                          // works
merge(value, { foo: 'bar' });              // works
merge(value, { bar: undefined });          // works
merge(value, { bar: 666 });                // works
merge(value, { foo: '', bar: undefined }); // works
merge(value, { foo: '', bar: 666 });       // works

merge(value, { foo: undefined }); // error!
//             ~~~ <-- undefined is not assignable to never
//  the expected type comes from property 'foo', 

那具有你想要的行為……雖然你得到的錯誤有點奇怪(理想情況下它會說 undefined 不能賦值給 string,但問題是編譯器知道傳入的類型是 undefined,而它希望類型是 string,所以編譯器將這些交集為 undefined & string,也就是 never。唉。

無論如何,這裡可能有一些警告;泛型函數在直接調用時效果很好,但它們組合得不好,因為 TypeScript 對高階類型的支持不是很好。我不知道這是否真的適用於你的使用情境,但這是在語言目前狀態下我能做到的最好方式。

程式碼的 Playground 連結

答案#2

在這種情況下,Pick 應該可以使用。

interface MyType {
  foo: string
  bar?: number
}

const merge = <K extends keyof MyType>(value1: MyType, value2: Pick<MyType, K>): MyType => {
  return {...value1, ...value2};
}

merge(value, {});                          // ok
merge(value, { foo: 'bar' });              // ok
merge(value, { bar: undefined });          // ok
merge(value, { bar: 666 });                // ok
merge(value, { foo: '', bar: undefined }); // ok
merge(value, { foo: '', bar: 666 });       // ok

merge(value, { foo: undefined });          // ng

遊樂場連結