Типы для удаления объекта по указанному c вложенному пути - PullRequest
0 голосов
/ 04 марта 2020
type Tail<T extends any[]> = ((...t: T) => void) extends ((
  h: any,
  ...r: infer R
) => void)
  ? R
  : never;

type DeepOmit<T, Path extends string[]> = T extends object
  ? {
      0: Omit<T, Path[0]>;
      1: {
        [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
      };
    }[Path['length'] extends 1 ? 0 : 1]
  : T;

У меня есть вышеупомянутый тип, который хорошо работает. Я проверил это следующим образом:

type A = DeepOmit<{ a: { b: { c: 1 } } }, ['a', 'b', 'c']>;

// {
//    a: {
//        b: Pick<{
//            c: 1;
//        }, never>;
//    };
// }

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

const del = <T extends object, K extends string[]>(src: T, path: K) => {
  // custom logic removed. just creating the result below.
  const result = {
    a: {
      b: {}
    }
  }
  return result as DeepOmit<T, K>
};

const deletedObj = del({a:{b:{c:1}}},["a","b","c"]);
// deletedObj.a.b.c <========== still works
// expected is deletedObj.a.b

Может кто-нибудь указать мне, в чем здесь проблема

Ответы [ 2 ]

2 голосов
/ 04 марта 2020

Проблема, с которой вы сталкиваетесь, заключается в том, что компилятор имеет тенденцию расширять кортежи для массивов, а литералы - для нелитералов, если вы не дадите ему конкретные подсказки. абонент из del() может использовать утверждение as const в параметре path, чтобы получить этот эффект (хотя вам нужно будет принять readonly string[], а не только string[]), но вы по-видимому, хотел бы, чтобы абонент не беспокоился об этом. Я подал microsoft / TypeScript # 30680 , чтобы попросить что-то вроде as const в подписи вызова, чтобы сделать это. При отсутствии такой функции вы должны полагаться на некоторые странные подсказки.

Если вы хотите, чтобы тип массива выводился как кортеж, если это возможно, вы должны включить тип кортежа в его контекст, например, через объединение. Так T extends Foo[] становится T extends Foo[] | []. Если вы хотите, чтобы тип выводился как строковый литерал, если это возможно, вы должны включить строковый литерал в его контекст или какой-либо другой string -подобный совет. Так что T extends string[] становится либо T extends (string | (""&{__:0}))[], либо, по моему предпочтению, T extends N[], где N - это еще один обобщенный c параметр N extends string. Вот как это выглядит:

declare const del: <T extends object, K extends N[] | [], N extends string>(
    src: T, path: K
) => DeepOmit<T, K>;

И вы можете увидеть, как оно работает так, как вы и планировали:

const deletedObj = del({ a: { b: { c: 1 } } }, ["a", "b", "c"]);
deletedObj.a.b; // okay
deletedObj.a.b.c; // error

Также обратите внимание, что ваше определение DeepOmit излишне окольным; {0: X, 1: Y}[T extends U ? 0 : 1] - это способ обойти ошибки, когда компилятор считает T extends U ? X : Y круговым (даже если этот вид обхода не поддерживается ). Но простая реализация DeepOmit не противоречит этим правилам: хорошо иметь рекурсивный тип, в котором рекурсия происходит в свойстве объекта:

type DeepOmit<T, Path extends string[]> = T extends object ?
    Path['length'] extends 1 ? Omit<T, Path[0]> : {
        [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
    } : T; // no error, still works

Хорошо, надеюсь что помогает; удачи!

Детская площадка ссылка на код

2 голосов
/ 04 марта 2020

Проблема связана с выводом типа второго аргумента. В примере с типом тип является строгим, как ['a','b','c'], но в аргументе функции выводится как string[], поэтому функция уровня типа DeepOmit вызывается с string[], а не с ['a','b','c']. Результат такой же, как:

type Result = DeepOmit<{ a: { b: { c: 1 } } }, string[]>;

Чтобы исправить это, нам нужно или строго ввести переменную, сказав ['a','b','c'] as ['a','b','c'], что очень многословно, или мы можем сделать функцию более строгой, обработав каждый аргумент отдельно (используя распространение). Недостатком является то, что API функции немного изменится (я бы сказал, даже в лучшую сторону), но вывод теперь работает, как и ожидалось. Рассмотрим:

const del = <T extends object, K extends string[]>(src: T, ...path: K) => {
  // custom logic removed. just creating the result below.
  const result = {
    a: {
      b: {}
    }
  }
  return result as DeepOmit<T, K>
};

const deletedObj = del({ a: { b: { c: 1 } } }, 'a', 'b', 'c');
deletedObj.a.b.c // error no c !
...