Машинопись: есть ли рекурсивный ключ?

Есть ли способ компилировать такой код и быть безопасным?

type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

interface MyReference {
  myKey: keyof ComplexObject;
}

const works1: MyReference = {
  myKey: "primitive1"
}

const works2: MyReference = {
  myKey: "complex"
}

const iWantThisToCompile1: MyReference = {
  myKey: "complex.primitive2" // Error: Type '"complex.primitive2"' is not assignable to type '"primitive1" | "complex"'.
}

const iWantThisToCompile2: MyReference = {
  myKey: "complex['primitive3']" // Error: Type '"complex['primitive3']"' is not assignable to type '"primitive1" | "complex"'.
}

// const iDontWantThisToCompile1: MyReference = {
//  myKey: "primitive2"
// }

// const iDontWantThisToCompile2: MyReference = {
//  myKey: "primitive3"
// }

Вы можете поиграть с этим кодом здесь.

Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
12
0
4 726
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Нет, к сожалению, Typescript не может этого сделать.

Редактировать: TS 4.1 добавил литералы шаблонов , см. ответ Дэвида Шеррета о том, как использовать их в рекурсивном типе

Единственное, что он поддерживает, это рекурсивный массив путей:

type Cons<H, T> = T extends readonly any[] ?
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
    : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
        P extends [] ? never : Cons<K, P> : never
    ) }[keyof T]
    : [];

type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

interface MyReference {
  myKey: Paths<ComplexObject>;
}

const works1: MyReference = {
  myKey: ["primitive1"]
}

const works2: MyReference = {
  myKey: ["complex"]
}

const iWantThisToCompile1: MyReference = {
  myKey: ["complex", "primitive2"]
}

const iWantThisToCompile2: MyReference = {
  myKey: ["complex", "primitive3"]
}

Библиотеки, такие как get lodash, работают как с вашим "complex.primitive2", так и с массивом путей, таких как ["complex", "primitive2"]. Поэтому, хотя это может быть не точный ответ, который вы ищете, надеюсь, он даст вам более безопасную альтернативу.

Я не думаю, что это точная копия, но вот ответ, из которого я получил псевдоним типа Paths: Определение типа TypeScript для пути к свойству объекта

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

Daniel Kaplan 17.12.2020 00:59

Оформить заказ Условные типы. Тип Paths активно их использует, и я уверен, что есть способ узнать, является ли тип значения расширением boolean, или использовать never, если нет. Я только что попытался изменить его сам, но не смог понять это...

Aaron 17.12.2020 01:36
Ответ принят как подходящий

Это возможно благодаря новым литеральным типам шаблонов и рекурсивным типам в TypeScript 4.1.

Тип доступа к свойству и индексу

Вот способ определения этого, который работает за пределами одного уровня. Можно использовать меньше типов, но этот подход не имеет дополнительных неиспользуемых параметров типа в общедоступном API.

export type RecursiveKeyOf<TObj extends object> = {
  [TKey in keyof TObj & (string | number)]:
    RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
}[keyof TObj & (string | number)];

type RecursiveKeyOfInner<TObj extends object> = {
  [TKey in keyof TObj & (string | number)]:
    RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];

type RecursiveKeyOfHandleValue<TValue, Text extends string> =
  TValue extends any[] ? Text :
  TValue extends object
    ? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
    : Text;

Только тип доступа к свойству

Если вам просто нужен доступ к свойству, это намного проще:

export type RecursiveKeyOf<TObj extends object> = {
  [TKey in keyof TObj & (string | number)]:
    TObj[TKey] extends any[] ? `${TKey}` :
    TObj[TKey] extends object
      ? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
      : `${TKey}`;
}[keyof TObj & (string | number)];

Объяснение и разбивка

export type RecursiveKeyOf<TObj extends object> = (
  (
    // Create an object type from `TObj`, where all the individual
    // properties are mapped to a string type if the value is not an object
    // or union of string types containing the current and descendant
    // possibilities when it's an object type.
    {
      // Does this for every property in `TObj` that is a string or number
      [TKey in keyof TObj & (string | number)]:
        RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
    }
  )[
    keyof TObj & (string | number) // for every string or number property name
  ] // Now flatten the object's property types to a final union type
);

// This type does the same as `RecursiveKeyOf`, but since
// we're handling nested properties at this point, it creates
// the strings for property access and index access
type RecursiveKeyOfInner<TObj extends object> = {
  [TKey in keyof TObj & (string | number)]:
    RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
}[keyof TObj & (string | number)];

type RecursiveKeyOfHandleValue<TValue, Text extends string> =
  // If the value is an array then ignore it, providing back
  // only the passed in text
  TValue extends any[] ? Text :
  // If the value is an object...
  TValue extends object
    // Then...
    // 1. Return the current property name as a string
    ? Text
      // 2. Return any nested property text concatenated to this text
      | `${Text}${RecursiveKeyOfInner<TValue>}`
    // Else, only return the current text as a string
    : Text;

Например:

// this type
{
  prop: { a: string; b: number; };
  other: string;
}

// goes to
{
  prop: "prop" | "prop.a" | "prop.b";
  other: "other";
}

// goes to
"prop" | "prop.a" | "prop.b" | "other"

О, это круто, я пытался сделать конкатенацию строк в типе раньше, но я, должно быть, делал это неправильно. Ааа это было до 4.1

Aaron 17.12.2020 01:39

Вы можете объяснить, как это работает? в идеале объясняя сначала поддержку доступа к свойствам, так как это проще

Daniel Kaplan 17.12.2020 05:07

@DanielKaplan Я добавил объяснение только для типа доступа к свойствам. Тип доступа к свойству и индексу следует аналогичным принципам.

David Sherret 17.12.2020 06:01

Когда одно из свойств является массивом (например, имена: строка[]), это решение также рекурсивно обращается к методам массива, что нежелательно. То есть тип RecursiveKeyOf включает в себя имена.concat, имена.значения и т.д. Как можно этого не допустить?

Marduk 09.04.2021 21:15

@Marduk Я обновил пример, чтобы справиться с этим. Один из способов справиться с этим — проверить, является ли значение массивом условного типа, и выйти раньше, чем просматривать его свойства.

David Sherret 02.12.2021 02:36

Когда вы ссылаетесь на такие типы, как Date, то также можно ссылаться на primitive1.getDate (демонстрация: tsplay.dev/mAjgRW). Это неправильная ссылка на ключ объекта.

dewey 09.02.2022 15:23

привет @DavidSherret в webstorm intellisense я получаю отличный результат, но все же компилятор предупреждает меня, когда я использую вложенную строку, например, «user.phone.number», и по прямому ключу ее передачи. у вас есть идея, в чем может быть проблема?

Roi Dayan 25.02.2022 17:34

«TS2589: экземпляр типа слишком глубокий и, возможно, бесконечный». - ТС 4.6.3

Kevin Beal 19.05.2022 20:20

@DavidSherret, возможно ли извлечь окончательный тип данных для возвращаемого типа? То есть: function foo<T>(bar: RecursiveKeyOf<T>): FinalValueOfRecursiveKey<bar>. По сути, если параметр bar равен 'prop.a' из вашего примера, я хочу, чтобы возвращаемый тип был string

user3534080 04.11.2022 01:56

О мой! Это определенно то, что я пытался сделать, но у меня не получилось. Большое спасибо! @ДэвидШеррет

mrabaev48 28.12.2022 09:52

Просто хотел зайти сюда и сказать, что если кто-то хочет включить элементы массива и его подразделы как 'arrayProp[0]' | 'arrayProp[0].a' | 'arrayProp[1]' | 'arrayProp[1].a' | ...` тогда просто замените extends any[] ? `${TKey}` : на extends any[] ? `${TKey}` | `${TKey}[${number}]` | `${TKey}[${number}].${RecursiveKeyOf<TObj[TKey][number]>}` :

5ar 26.01.2023 16:50

Это фантастическая работа. Единственный синтаксис Typescript, который я не понимаю (и не могу найти ссылку), — это когда вы сглаживаете тип объекта в его свойствах, используя { ... }[keyof TObj & (string | number)]. Какую функцию Typescript вы там используете?

sedge 13.03.2023 20:08

Это должно быть выполнимо с литералами шаблона:

type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

type PathOf<T> =  {
  [K in keyof T]: T[K] extends object ? K | `${K}.${PathOf<T[K]>}` | `${K}['${PathOf<T[K]>}']` : K
}[keyof T]

type PathOfComplexObject = PathOf<ComplexObject>

Площадка для машинописи

На игровой площадке есть некоторые жалобы, но если вы наведете курсор на PathOfComplexObject, вы увидите сгенерированные типы. Я понимаю, почему он жалуется:

Реализация типа слишком глубокая и, возможно, бесконечная.

но я не уверен в:

Тип «K» нельзя присвоить типу «string | номер | большой | логический | ноль | неопределенный'.

PathOf все равно потребуется немного больше внимания, если вы хотите сохранить обозначение скобок, поскольку добавление еще одного слоя к .complex может привести к типу complex['primitive2.foo'] и другим.
chautelly 17.12.2020 01:51

По-видимому, пересечение K с string решает проблему not assignable to.

chautelly 17.12.2020 01:59

Я получил помощь в другом месте и получил этот тип:

type ComplexObject = {
  primitive1: boolean;
  complex: {
    primitive2: string;
    primitive3: boolean;
  }
};

type RecKeyof<T, Prefix extends string = never> =  
  T extends string | number | bigint | boolean 
  | null | undefined | ((...args: any) => any ) ? never : {
  [K in keyof T & string]: [Prefix] extends [never] 
    ? K | `['${K}']` | RecKeyof<T[K], K> 
    : `${Prefix}.${K}` | `${Prefix}['${K}']` | RecKeyof<T[K],`${Prefix}.${K}` | `${Prefix}['${K}']`>
}[keyof T & string];

interface MyReference {
  myKey: RecKeyof<ComplexObject>;
}

const works1: MyReference = {
  myKey: "primitive1"
}

const works2: MyReference = {
  myKey: "complex"
}

const iWantThisToCompile1: MyReference = {
  myKey: "complex.primitive2"
}

const iWantThisToCompile2: MyReference = {
  myKey: "complex['primitive3']"
}

// const iDontWantThisToCompile1: MyReference = {
//  myKey: "primitive2"
// }

// const iDontWantThisToCompile2: MyReference = {
//  myKey: "primitive3"
// }

Здесь вы можете увидеть, как это работает.

Вот тип с лучшей документацией:

type RecKeyof<T, Prefix extends string = ""> = 
  // If T matches any of the types in the union below, we don't care about its properties.
  // We must exclude functions, otherwise we get infinite recursion 'cause functions have
  // properties that are functions: i.e. myFunc.call.call.call.call.call.call...
  T extends string | number | bigint | boolean | null | undefined | ((...args: any) => any ) 
    ? never // skip T if it matches
    // If T doesn't match, we care about it's properties. We use a mapped type to rewrite
    // T.
    // If T = { foo: { bar: string } }, then this mapped type produces
    // { foo: "foo" | "foo.bar" }
    : {
      // For each property on T, we remap the value with
      [K in keyof T & string]: 
        // either the current prefix.key or a child of prefix.key.
        `${Prefix}${K}` | RecKeyof<T[K],`${Prefix}${K}.`>
    // Once we've mapped T, we only care about the values of its properties
    // so we tell typescript to produce the union of the mapped types keys.
    // { foo: "1", bar: "2" }["foo" | "bar"] generates "1" | "2"
    }[keyof T & string];

Другие вопросы по теме