簡體   English   中英

TypeScript: Generic extends object with only optional keys

[英]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過載之前出現,以便它具有更高的優先級。 如果您檢查此代碼,上面的fUndeffSomeReqfNoReq會按照您期望的方式輸入。


另一種可能的方法是只給出一個調用簽名並使用條件類型來計算 function output 類型。 像這樣的東西:

function someFunc<P extends object | undefined>(): (...params:
    P extends undefined ? [] :
    Partial<P> extends P ? [P?] :
    [P]
) => string;
function someFunc() {
    return null!
}

這應該表現得非常相似(你會看到fUndeffSomeReqfNoReq是正確的類型),但允許我們更直接地編寫我們的邏輯: 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!
}

這可能更明顯地與您的過載有關。 無論哪種方式都應該有效。


好的,希望有幫助; 祝你好運!

Playground 代碼鏈接

我知道這有點老了,但取決於你到底想要什么,你可以選擇:

  1. 僅必填字段。
  2. 只有可選字段。
  3. 將所有字段強制轉換為可選。

這些選項中的每一個都可以是淺的(僅頂級鍵)或深的(上一級鍵的每個嵌套鍵)。

在使用這些 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM