我一直在为下面的函数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
的单一对象类型,不如说是每种可能组合的并集。有没有更简单的方法来执行我要执行的操作或将这些组合放平?
截屏具有产生的确切类型:
我想要的#2的类型是
{
readonly maxLength?: string | undefined;
readonly prefix: string;
readonly suffix: string;
}
阅读@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">
如果内联具有默认值的对象,这也可以正常工作,从而消除了对键入常量的需求。