Тип TypeScript для динамической маркировки определенных свойств типа объекта как «обязательный» и «определенный»? - PullRequest
1 голос
/ 25 сентября 2019

Я пытался придумать универсальные типы для функции 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: ""

Я хочу сделать этонастолько динамичный, насколько это возможно (максимально возможное число типов).

Пока этот код компилируется, типизация отключена.Вместо того, чтобы при необходимости удалять один тип объекта с undefined s, это объединение каждой возможной комбинации.Есть ли более простой способ сделать то, что я пытаюсь сделать, или сгладить эти комбинации?

На скриншотах указаны точные типы:

  1. Один ключ работает нормально
  2. Два ключа производят уродливые типы объединения

Мой желаемый тип для # 2 -

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

1 Ответ

0 голосов
/ 27 сентября 2019

После прочтения предложения @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>;
};

use.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">

Он также работает правильно, если объект со значениями по умолчанию встроен, что исключаетпотребность в набранной константе.

...