[英]TypeScript: Generic extends object with only optional keys
我有一個過載的 function
export function someFunc<P extends undefined>(): () => string
export function someFunc<P extends object>(): (params: P) => string
export function someFunc<P extends object | undefined>() {
// ...implementation
}
我想要實現的是能夠為P
沒有任何必需鍵的情況添加一個重載,例如
const obj: P = { foo?: string, bar?: string } // matches
const obj2: P = { foo: string, bar?: string } // doesn't match
重載看起來像這樣:
export function someFunc<P extends NoOptionalKeys>(): (params?: P) => string
嗯,我不知道我是否見過以這種方式使用的重載; 我想您希望調用者手動指定P
泛型,然后讓編譯器從中選擇調用簽名,因為調用簽名都是零參數,並且沒有任何東西可以自動推斷P
所以我假設你正在尋找這樣的東西:
const fUndef = someFunc<undefined>();
// () => string
const fSomeReq = someFunc<{ a: string, b?: string }>();
// (params: {a: string, b?: string}) => string;
const fNoReq = someFunc<{ a?: string, b?: string }>();
// (params?: {a?: string, b?: string}) => string;
使用重載執行此操作有點奇怪,因為通用約束是上限。 P extends XXX
意味着P
必須是XXX
的某個子類型(所以它應該是XXX
,或者比XXX
更窄的東西)。
但是在 TypeScript 中,具有所有可選屬性的 object 是具有任何必需屬性的相同 object 的超類型,而不是子類型。 所以你要找的東西本質上是一個下限通用約束,但 TypeScript 並不直接支持它。 (有關更多信息,請參閱microsoft/TypeScript#14520 。)您可能想說P super Partial<P>
而不是P extends Partial<P>
。 或者,等效地, Partial<P> extends P
,但通用約束不能那樣工作。
您可以使用條件類型模擬下限約束,因此您可以編寫P extends (LowerBound extends P? UpperBound: never)
而不是P extends UpperBound super LowerBound
,並且它有機會工作。 對你來說,這意味着:
function someFunc<P extends undefined>(): () => string;
function someFunc<P extends (Partial<P> extends P ? object : never)>(): (params?: P) => string
function someFunc<P extends object>(): (params: P) => string;
function someFunc() {
return null!
}
我把它作為第二個重載; 它需要在常規的object
過載之前出現,以便它具有更高的優先級。 如果您檢查此代碼,上面的fUndef
、 fSomeReq
和fNoReq
會按照您期望的方式輸入。
另一種可能的方法是只給出一個調用簽名並使用條件類型來計算 function output 類型。 像這樣的東西:
function someFunc<P extends object | undefined>(): (...params:
P extends undefined ? [] :
Partial<P> extends P ? [P?] :
[P]
) => string;
function someFunc() {
return null!
}
這應該表現得非常相似(你會看到fUndef
、 fSomeReq
和fNoReq
是正確的類型),但允許我們更直接地編寫我們的邏輯: Partial<P> extends P
而不是相反,因為P
是已經由調用者確定,而不是驗證約束,我們正在使用條件類型對其進行測試。
請注意,上面的返回類型是單個可調用的,帶有一個有條件確定的元組類型 rest 參數。 我這樣做是因為所有三種 output 類型都是返回string
的可調用對象。 你可以這樣寫:
function someFunc<P extends object | undefined>():
P extends undefined ? () => string :
Partial<P> extends P ? (params?: P) => string :
(params: P) => string
function someFunc() {
return null!
}
這可能更明顯地與您的過載有關。 無論哪種方式都應該有效。
好的,希望有幫助; 祝你好運!
我知道這有點老了,但取決於你到底想要什么,你可以選擇:
這些選項中的每一個都可以是淺的(僅頂級鍵)或深的(上一級鍵的每個嵌套鍵)。
在使用這些 util 類型之前,我已經這樣做了:
/*************************************
*** Sub-utils for ease of (re)use ***
*** Optional, makes code more DRY ***
*************************************/
/**
* Same as Nullable except without `null`.
*/
export type Optional<T> = T | undefined;
/**
* Opposite of built-in `NonNullable`.
*/
export type Nullable<T> = Optional<T> | null;
/**
* Types that can be used to index native JavaScript types, (Object, Array, etc.).
*/
export type IndexSignature = string | number | symbol;
/**
* An object of any index-able type to avoid conflicts between `{}`, `Record`, `object`, etc.
*/
export type Obj<O extends Record<IndexSignature, unknown> | object = Record<IndexSignature, unknown> | object> = {
[K in keyof O as K extends never
? never
: K
]: K extends never
? never
: O[K] extends never
? never
: O[K];
} & Omit<O, never>;
/**
* Any type that is indexable using `string`, `number`, or `symbol`.
*
* Serves as a companion to {@link OwnKeys} while maintaining the generalizable usage of {@link Obj}.
*/
export type Indexable<ValueTypes = unknown> = (
{
[K: IndexSignature]: ValueTypes;
}
| Obj
);
/********************************************
*** Actual typedefs to achieve your goal ***
********************************************/
/**
* Picks only the optional properties from a type, removing the required ones.
* Optionally, recurses through nested objects if `DEEP` is true.
*/
export type PickOptional<T, DEEP extends boolean = false> = { // `DEEP` must be false b/c `never` interferes with root level objects with both optional/required properties
// If `undefined` extends the type of the value, it's optional (e.g. `undefined extends string | undefined`)
[K in keyof T as undefined extends T[K]
? K
: never
]: DEEP extends false
? T[K]
: T[K] extends Optional<Indexable> // Like above, we must include `undefined` so we can recurse through both nested keys in `{ myKey?: { optionalKey?: object, requiredKey: object }}`
? PickOptional<T[K], DEEP>
: T[K];
};
/**
* Picks only the required fields out of a type, removing the optional ones.
* Optionally, recurses through nested objects if `DEEP` is true.
*/
export type PickRequired<T, DEEP extends boolean = false> = {
[K in keyof T as K extends keyof PickOptional<T, DEEP>
? never
: K
]: T[K] extends Indexable
? PickRequired<T[K], DEEP>
: T[K];
};
/**
* Companion to built-in `Partial` except that it makes each nested property optional
* as well.
*
* Each non-object key's value will be either:
* - If `NT` (NewType) is left out, then the original type from `T` remains.
* - The specified `NT` type.
*/
export type PartialDeep<T, NT = never> = T extends Indexable
? {
// `?:` forces the key to be optional
[K in keyof T]?: T[K] extends Indexable
? Nullable<PartialDeep<T[K], NT>>
: NT extends never
? Nullable<T[K]>
: Nullable<NT>
}
: NT extends never
? Nullable<T>
: Nullable<NT>;
對於您的情況,您可以像這樣使用它們:
// Includes both `P extends undefined` and `P extends object`
export function someFunc<P extends Optional<Indexable>>(params: P): string;
// Only includes optional keys from `P extends object`
export function someFunc<P extends PickOptional<Indexable>>(params: P): string;
// What I think you're seeking which is that every key is optional
export function someFunc<P extends PartialDeep<Indexable>>(params: P): string;
// Actual implementation needs to allow for each of:
// - undefined
// - object
// - PickOptional<object>
// - PartialDeep<object>`
export function someFunc<P extends Optional<PartialDeep<Indexable>>>(params: P) {
return params ? JSON.stringify(params) : 'No params';
}
雖然我添加了一個不完整的實現,但它至少展示了如何使用重載和類型實用程序。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.