【问题标题】:TypeScript type to dynamically mark certain properties of an object type as "required" and "defined"?TypeScript 类型将对象类型的某些属性动态标记为“必需”和“已定义”?
【发布时间】:2019-09-25 16:14:08
【问题描述】:

我一直在尝试为下面的函数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 的可能性。这避免了函数开发者必须使用非空断言,同时也不会给函数用户带来负担。

所需用法示例:

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: ""

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

虽然这段代码确实可以编译,但输入却以某种方式关闭。它不是在需要的地方删除undefineds 的单一对象类型,而是看起来是每个可能组合的联合。有没有更简单的方法来做我想做的事情或将这些组合变平?

屏幕截图具有生成的确切类型:

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

#2 我想要的类型是

{
    readonly maxLength?: string | undefined;
    readonly prefix: string;
    readonly suffix: string;
}

【问题讨论】:

  • 这并不完全构成minimal reproducible example,因为我想您的问题类似于“EnforcedDefaultProps&lt;FormatOpts, "prefix" | "suffix"&gt; 不是我期望的类型”,但这并未在任何地方列出在您的代码中,还有一堆额外的代码掩盖了关键问题。您是否可能只需要type EnforcedDefaultProps&lt;P, K extends keyof P&gt; = [K] extends [never] ? P : Omit&lt;P, K&gt; &amp; RequiredAndDefined&lt;P, K&gt;;?也就是说,不要distribute the conditional type超过K
  • 谢谢。经过更多的实验,我现在期望至少在我的一种用法上打字。发布作为参考答案。

标签: typescript generics type-inference


【解决方案1】:

在阅读了@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">

如果内联具有默认值的对象,它也可以正常工作,从而消除对类型常量的需要。

【讨论】:

  • 稍微清理了解决方案并为此创建了一个 npm 包:use-defaults
猜你喜欢
  • 1970-01-01
  • 2022-01-17
  • 1970-01-01
  • 2019-04-05
  • 2020-04-14
  • 1970-01-01
  • 2016-09-15
  • 2021-01-09
  • 2021-07-06
相关资源
最近更新 更多