TypeScript类型可将对象类型的某些属性动态标记为“必需”和“定义”?

问题描述 投票:1回答:1

我一直在为下面的函数useDefaults提出通用类型:

type ValuesOf<T extends readonly any[] | undefined> = T extends readonly any[] ? T[number] : never;
type RequiredAndDefined<T, K extends keyof T> = {
    [P in K]-?: Exclude<T[P], undefined>;
};

export type EnforcedDefaultProps<P, K extends keyof P> = K extends never ? P : Omit<P, K> & RequiredAndDefined<P, K>;
export type ArrayEnforcedDefaultProps<P, K extends ReadonlyArray<keyof P> | undefined> = EnforcedDefaultProps<P, ValuesOf<K>>;

export const useDefaults = <P extends object, KEYS extends keyof P>(
    props: P | undefined,
    defaultProps: ArrayEnforcedDefaultProps<P, typeof enforcedDefaults>,
    enforcedDefaults?: ReadonlyArray<KEYS>,
): ArrayEnforcedDefaultProps<P, typeof enforcedDefaults> => {
    const newProps: P = { ...defaultProps, ...props };
    if (enforcedDefaults) { // if user explicitly passes undefined to the prop, default prop will not be used unless the key is in enforcedDefaults
        enforcedDefaults
            .filter((key) => newProps[key] === undefined)
            .forEach((key) => newProps[key] = defaultProps[key]);
    }
    return newProps as ArrayEnforcedDefaultProps<P, typeof enforcedDefaults>;
};

此函数将对两个对象进行简单的基于分配的操作:一个包含用户提交的选项,另一个包含开发人员设置的默认值。然后,它将为某些属性(提供了功能开发人员)可选地强制使用未定义的值,以便useDefaults的返回对象的类型避免了这些某些属性中的每一个的值为undefined的可能性。这避免了函数开发人员必须使用非null断言,但也不会给函数用户带来负担。

示例所需用法:

interface FormatOpts {
    maxLength?: number,
    prefix?: string,
    suffix?: string,
}

const format = (value: string, _opts?: FormatOpts) => {
    const opts = useDefaults(_opts, {
        prefix: "",
        suffix: "",
    }, ["prefix", "suffix"]);

    // prefix and suffix are guaranteed to not be `undefined` at this point, but are not explicitly required to be specified in the `_opts` object by the user of the function

    const modifiedPrefix = prefix.toUpperCase(); // want to avoid things like prefix!.toUpperCase();
}

// examples with "prefix" prop as a focus:
format("ASDF"); // OK -> prefix after useDefaults: ""
format("ASDF", { prefix: "MyPrefix" }); // OK -> prefix after useDefaults: "MyPrefix"
format("ASDF", { prefix: undefined }); // OK -> prefix after useDefaults: ""
format("ASDF", { suffix: "test" }); // OK -> prefix after useDefaults: ""

我希望使它尽可能动态(尽可能多地推断出类型)。

尽管此代码确实可以编译,但是键入却有所不同。与其在需要时删除undefined的单一对象类型,不如说是每种可能组合的并集。有没有更简单的方法来执行我要执行的操作或将这些组合放平?

截屏具有产生的确切类型:

  1. One key works fine
  2. Two keys produce ugly union types

我想要的#2的类型是

{
    readonly maxLength?: string | undefined;
    readonly prefix: string;
    readonly suffix: string;
}
typescript generics type-inference
1个回答
0
投票

阅读@jcalz建议并进行了更多的实验/简化后,这几乎动态地生成了正确的键入:

types.ts:

import { StrictOmit } from "ts-essentials";

export type RequiredAndDefined<T, K extends keyof T> = {
    [P in K]-?: Exclude<T[P], undefined>;
};
export type MappedObjValue<A, B> = {
    [K in keyof A & keyof B]:
    A[K] extends B[K]
        ? never
        : K
};
export type OptionalKeys<T> = (MappedObjValue<T, Required<T>>)[keyof T];
export type KeyOrKeysOf<P> = (ReadonlyArray<keyof P>) | (keyof P);
export type ExtractArrayItem<T> = T extends ReadonlyArray<infer U> ? U : T;

export type EnforcedDefaultProps<
    P extends object,
    K extends (KeyOrKeysOf<P> | undefined),
    EK = ExtractArrayItem<K>
> = [EK] extends [keyof P] ? StrictOmit<P, EK> & RequiredAndDefined<P, EK> : P;

export type EnforcedDefaultPropsInput<
    P extends object,
    K extends (KeyOrKeysOf<OP> | undefined),
    OP extends object = Pick<P, OptionalKeys<P>>,
    EK = ExtractArrayItem<K>
> = [EK] extends [keyof OP] ? EnforcedDefaultProps<OP, EK> : OP;

useDefaults.ts:

export const useDefaults = <P extends object, ED extends Array<OptionalKeys<P>> | undefined>(
    props: P,
    defaultProps: EnforcedDefaultPropsInput<P, ED>,
    enforcedDefaults?: ED,
): EnforcedDefaultProps<P, ED> => {
    const newProps: P = { ...defaultProps, ...props };
    if (enforcedDefaults) { // if user explicitly passes undefined to the prop, default prop will not be used unless the key is in enforcedDefaults
        enforcedDefaults
            .filter((key) => newProps[key] === undefined)
            .forEach((key) => {
                newProps[key] = (defaultProps as any)[key]; // cast to any for now
            });
    }
    return newProps as EnforcedDefaultProps<P, ED>;
};

usage.ts:

interface ISharedFormatOpts {
    prefix?: string,
    suffix?: string,
}

export interface IStringFormatOpts extends ISharedFormatOpts {
    maxLength?: number,
}

export const defaultStringFormatOpts: DeepReadonly<EnforcedDefaultPropsInput<IStringFormatOpts, "prefix" | "suffix">> = {
    prefix: "",
    suffix: "",
};

// ...

const options = useDefaults(opts as IStringFormatOpts, defaultStringFormatOpts, ["prefix", "suffix"]);

options的解析类型是

Pick<IStringFormatOpts, "maxLength"> & RequiredAndDefined<IStringFormatOpts, "prefix" | "suffix">

如果内联具有默认值的对象,这也可以正常工作,从而消除了对键入常量的需求。

© www.soinside.com 2019 - 2024. All rights reserved.