TypeScript: Позиционные ИЛИ Именованные параметры в конструкторе

У меня есть класс, который в настоящее время принимает более 7 позиционных параметров.

class User {
  constructor (param1, param2, param3, …etc) {
    // …
  }
}

Я хочу преобразовать это в именованные параметры с помощью объекта параметров.

type UserOptions = {
  param1: string
  // …
}

class User {
  constructor ({ param1, param2, param3, …etc } = {}: UserOptions) {
    // …
  }
}

Это нормально, за исключением того, что теперь необходимо переработать множество тестов, чтобы изменить подпись, поэтому я хотел бы поддерживать как именованные, так и позиционные параметры.

Я могу сделать так, чтобы код поддерживал оба, но я не уверен, как добавить туда типы, не выписывая все типы дважды. В идеале список типов в моем UserOptions стал бы позиционными параметрами в том порядке, в котором они определены в UserOptions.

Есть ли способ сделать что-то подобное?

type UserOptions = {
  param1: string
  // …
}

type ToPositionalParameters<T> = [
  // ??? something like ...Object.values(T)
  // and somehow getting the keys as the positional argument names???
]

type PositionalUserOptions = ToPositionalParameters<UserOptions>

class User {
  constructor (...args: PositionalUserOptions);
  constructor (options: UserOptions);
  constructor (...args: [UserOptions] | PositionalUserOptions) { 
    // …
  }
}

Также можно было бы рассмотреть решение, работающее наоборот, то есть позиционные аргументы для named, возможно, это проще?


person timoxley    schedule 10.02.2021    source источник
comment
Немного поздно для вас, но VSCode поддерживает рефакторинг для преобразования параметров в деструктурированный объект для функций. Это также обновляет использование.   -  person spender    schedule 10.02.2021
comment
@spender oh wow У меня также есть эта опция в vim через coc-typescript. Спасибо!   -  person timoxley    schedule 10.02.2021
comment
Вам обязательно нужно перегрузить конструктор? Также может работать отдельная фабричная функция.   -  person Bergi    schedule 11.02.2021
comment
Конечно, фабричная функция также будет работать, но с той же проблемой, я хочу посмотреть, есть ли способ избежать повторения определений типов для каждого параметра.   -  person timoxley    schedule 11.02.2021


Ответы (2)


Здесь вас блокируют несколько вещей.

Во-первых, порядок свойств в типе объекта в настоящее время ненаблюдаемый в TypeScript, потому что они не влияют на возможность назначения. Например, в системе типов нет разницы между типом {a: string, b: number} и {b: number, a: string}. Есть грязные уловки, которые можно использовать, чтобы извлечь информацию из компилятор, чтобы увидеть, представляет ли он ключи как ["a", "b"] против ["b", "a"], но все, что он делает, это дает вам некоторый порядок при компиляции; при чтении объявления типа сверху вниз не гарантируется, что порядок будет одинаковым при каждой компиляции! См. microsoft / TypeScript # 17944 для открытой проблемы, требующей согласованного упорядочивания. . И см. microsoft / TypeScript # 42178, где приведен пример, казалось бы, несущественного изменения кода, которое меняет заказ от того, что ожидается. На данный момент мы не можем каким-либо согласованным образом автоматически преобразовать тип объекта в упорядоченный кортеж.

Во-вторых, имена аргументов в типе функции намеренно недоступны как строковые литералы. Они ненаблюдаемы по тем же причинам, что и упорядочение свойств объекта: они не влияют на возможность назначения. Например, нет разницы между типом функции (foo: string) => void и (bar: string) => void в системе типов. Я даже не думаю, что есть какие-то уловки, которые могли бы раскрыть такие подробности. В системе типов имена аргументов функций используются только в качестве документации или IntelliSense. Вы можете преобразовать аргументы именованной функции в помечены элементы кортежа, но не более того. См. этот комментарий в microsoft / TypeScript # 28259, где объясняется, что мы можем: • использовать имена параметров сигнатуры вызовов или метки кортежей в качестве строк в TypeScript. На данный момент мы не можем автоматически превратить помеченный кортеж в тип объекта, где ключи объекта соответствуют меткам кортежа.


Вы можете как бы обойти обе эти проблемы, если вместо попытки преобразовать объект в кортеж или кортеж в объект, мы предоставим достаточно информации, чтобы сделать и то, и другое:

const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [string, number, boolean];

Здесь userOptionKeys - явный упорядоченный список желаемых ключей в UserOptions объектах ... в том же порядке, что и в нашем вручную созданном кортеже PositionalUserOptions. Теперь, когда у нас есть имена ключей, мы можем построить UserOptions:

type UserOptions = { [I in Exclude<keyof PositionalUserOptions, keyof any[]> as
  typeof userOptionKeys[I]]: PositionalUserOptions[I] }
/* type UserOptions = {
    param1: string;
    param2: number;
    thirdOne: boolean;
} */

И пока мы это делаем, мы можем написать функцию для преобразования кортежа типа PositionalUserOptions в объект типа UserOptions (с утверждением типа any, чтобы компилятор не пытался его проверить, чего он не может сделать. без труда):

function positionalToObj(opts: PositionalUserOptions): UserOptions {
  return opts.reduce((acc, v, i) => (acc[userOptionKeys[i]] = v, acc), {} as any)
}

Теперь мы можем написать этот User класс, используя positionalToObj в реализации конструктора для нормализации вещей:

class User {
  constructor(...args: PositionalUserOptions);
  constructor(options: UserOptions);
  constructor(...args: [UserOptions] | PositionalUserOptions) {
    const opts = args.length === 1 ? args[0] : positionalToObj(args);
    console.log(opts);
  }
}

new User("a", 1, true);
/* {
  "param1": "a",
  "param2": 1,
  "thirdOne": true
}  */

Оно работает! С точки зрения системы типов и возможности присваивания это лучшее, что вы можете сделать. С точки зрения документации / IntelliSense это не очень хорошо. Когда вы вызываете new User(), в документации для многопараметрической версии вам будут предоставлены такие ярлыки, как args_0 и args_1. Если вы хотите, чтобы это были param1 и param2, вам просто нужно укусить пулю и дважды написать имена параметров; один раз в виде строковых литералов и снова в виде меток кортежей, поскольку нет возможности преобразовать один в другой:

const userOptionKeys = ["param1", "param2", "thirdOne"] as const;
type PositionalUserOptions = [param1: string, param2: number, thirdOne: boolean];

Стоит ли оно того? Может быть ... решать тебе.

Ссылка для игровой площадки на код

person jcalz    schedule 11.02.2021

Вы можете сделать это, но инструменты, которые вы должны использовать, не одобряются командой TypeScript и полагаются на порядок создания экземпляров определенных внутренних типов (по крайней мере, это то, что предполагает проблему, запрашивающую операцию объединения в кортеж).

Тем не менее, было весело разобраться в этом, так что готово:

// Via https://github.com/Microsoft/TypeScript/issues/13298#issuecomment-707364842
type UnionToTuple<T> = (
    (
        (
            T extends any
                ? (t: T) => T
                : never
        ) extends infer U
            ? (U extends any
                ? (u: U) => any
                : never
            ) extends (v: infer V) => any
                ? V
                : never
            : never
    ) extends (_: any) => infer W
        ? [...UnionToTuple<Exclude<T, W>>, W]
        : []
);

type Head<T> = T extends [infer A, ...any] ? A : never
type Tail<T extends any[]> = T extends [any, ...infer A] ? A : never;

type PluckFieldTypes<T extends object, Fields extends any[] =
  UnionToTuple<keyof T>> = _PluckFieldType<T, Head<Fields>, Tail<Fields>, []>
type _PluckFieldType<
  T extends object,
  CurrentKey,
  RemainingKeys extends any[],
  Result extends any[]
  > = RemainingKeys['length'] extends 0
      ? CurrentKey extends keyof T ? [...Result, T[CurrentKey]] : never
      : CurrentKey extends keyof T
        /* && */? RemainingKeys extends (keyof T)[]
        ? [...Result, T[CurrentKey], ..._PluckFieldType<T, Head<RemainingKeys>, Tail<RemainingKeys>, []>]
        : never : never;

// -- IMPL --
type Args = {
  param1: string,
  param2: number,
  param3: Date,
  param4: number,
  param5: string,
  param6: Date,
  param7: boolean,
  param8: boolean,
  param9: null,
  param10: 'abc' | 'xyz'
}

class CompositeClass {
  constructor(params: Args);
  constructor(param1: string, param2: number, param3: Date);
  constructor(...args: [Args] | PluckFieldTypes<Args>) {
  }
}

Предполагаемый тип ...args оказывается (parameter) args: [Args] | [string, number, Date, number, string, Date, boolean, boolean, null, "abc" | "xyz"], а под вторым конструктором появляется красная волнистая линия, пока вы не добавите все остальные параметры.

Единственное, что мы не можем получить, - это требование, чтобы имена аргументов соответствовали именам полей (но это потому, что помеченные кортежи не делают их метки доступными на уровне типа, поэтому вы не можете прочитать их из встроенные ConstructorArguments или напишите их здесь с помощью _PluckFieldType.)

person Sean Vieira    schedule 11.02.2021