Пересечение и создание подклассов для сужения типа свойства Record<string, Any>`

Я имею дело с внешне определенным типом и постоянной переменной, которая имеет общее свойство user_data, определенное как:

// in an external .d.ts definition

declare class Foo {
    readonly user_data: Record<string, any>

    // other members including private
    private __secret(): void;
}

declare function doSomething(foo: Foo): void;

declare const foo: Foo;

Моя цель — сузить тип user_data, включив в него только те свойства, которые я собираюсь там хранить. До сих пор мне удавалось добиться этого, создав новый интерфейс, расширяющий исходный внешний класс:

interface MyFoo extends Foo {
    readonly user_data: {
        a: string,
        b: number,
    }
}

declare const foo: MyFoo;

foo.user_data.a = "hello"; // ok
foo.user_data.a = 1; // error - expected
foo.user_data.c = "hello"; // error - expected
doSomething(foo); // ok

Однако я хочу избежать необходимости каждый раз определять новый интерфейс, поскольку это повторяющийся шаблон с множеством разных типов Foo. Шаблон, который в идеале хотелось бы сохранить простым и инкапсулировать в общий шаблон.

До сих пор я пробовал:

// generic utility type
type HasUserData<T, U> = T & {user_data: U};

declare const foo: HasUserData<Foo, {
    a: string,
    b: number,
}>;

foo.user_data.a = "hello"; // ok
foo.user_data.a = 1; // error - expected
foo.user_data.c = "hello"; // ok - BAD!
doSomething(foo); // ok

Я предполагаю, что проблема с вышеизложенным заключается в том, что:

declare const x: Record<string, any> & {y: string};
x.z; // okay, but unexpected. I'd expect that with a | (union) not &

Я также пробовал:

// generic utility type
type HasUserData<T, U> = Omit<T, "user_data"> & {user_data: U};

declare const foo: HasUserData<Foo, {
    a: string,
    b: number,
}>;

foo.user_data.a = "hello"; // ok
foo.user_data.a = 1; // error - expected
foo.user_data.c = "hello"; // error - expected
doSomething(foo); // error: missing private members - BAD!

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

У вас может быть единый интерфейс, в котором вы можете изменить общий тип, который вы назначите user_data, посмотрите, поможет ли это вам: Детская площадка

HairyHandKerchief23 25.08.2024 12:53

Пожалуйста, отредактируйте это, чтобы уточнить, в чем именно заключается ваш вопрос. Вы спрашиваете, почему Record<string, any> & {y: string} позволяет вам получить доступ к z собственности? Если да, то Record<string, any> именно это и делает. Это позволяет вам получить доступ к любому свойству вообще. Возможно, вы хотите object вместо Record<string, any>. Или вы спрашиваете, как заставить Foo и его подтипы действовать так, как вы хотите? Если да, то я бы посоветовал вам просто сделать Foo общий, как показано по ссылке на эту игровую площадку . Или вы спрашиваете что-то другое? В любом случае, пожалуйста, отредактируйте, чтобы задать один четко сформулированный основной вопрос.

jcalz 25.08.2024 17:43
Foo — это часть внешней структуры, которую я должен использовать, но не могу контролировать. Фреймворк имеет множество типов, которые следуют тому же шаблону, что и Foo, т. е. предоставляют свойство user_data, определенное как Record<string, any>. Моя цель — сузить/ограничить тип user_data, чтобы он мог иметь только те свойства, которые я хочу. И сделать это без объявления интерфейса, который каждый раз расширяет каждый конкретный Foo.
Levi Haskell 25.08.2024 20:02

В посте до сих пор нет ни одного четко определенного вопроса. Из заголовка "пересечение с типом записи" и "а зачем?" прокомментируйте, похоже, вы спрашиваете о поведении или Record<string, any> & {y: string} и о подтипах Foo и частных свойствах и Omit - все это посторонние. Или, что более вероятно, вы действительно спрашиваете о подтипах Foo, а все остальное — неудачные попытки этого добиться. Не могли бы вы отредактировать, чтобы удалить все постороннее в вашем вопросе и/или сделать так, чтобы читатель сразу понял, в чем заключается вопрос?

jcalz 25.08.2024 20:36

(см. предыдущий комментарий). Предполагая, что речь идет не только о пересечениях: вы не можете программно манипулировать типами классов с закрытыми/защищенными членами так, как вы хотите. Если вы не можете изменить Foo, вы можете подклассифицировать его, как показано по этой ссылке на игровую площадку , а затем обратиться к общему подклассу. Таким образом, вы можете просто использовать аргумент универсального типа для типа подкласса. (И обратите внимание: подкласс может существовать только как тип в TS и на самом деле не обязательно должен существовать как конструктор). Я был бы рад написать такой ответ, если вопрос отредактировать, чтобы перестать спрашивать о перекрестках.

jcalz 25.08.2024 20:41

Дополнительные вопросы перефразированы. Моя цель, как я пытаюсь объяснить выше, состоит в том, чтобы: 1. Ограничить тип user_data произвольного внешнего Foo типа возможными непубличными членами 2. Сделать это без изменения внешних .d.ts определений (как показано выше) 3. Избегать создавая подклассы для каждого конкретного Foo и вместо этого используйте общий тип утилиты. Остальное — мои неудачные попытки достичь этого. Хотя я думаю, что они актуальны.

Levi Haskell 25.08.2024 21:05

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

Levi Haskell 25.08.2024 21:12

Название по-прежнему «Пересечение с типом записи», поэтому я в замешательстве. Я могу написать ответ, но только тогда, когда я уверен, что понимаю вопрос и что мой потенциальный ответ касается его. Сейчас я все еще в замешательстве, извини.

jcalz 25.08.2024 21:12

Обновлен заголовок, надеюсь, это поможет. Тай

Levi Haskell 25.08.2024 21:25

Создание подклассов и/или расширение интерфейса позволяет сузить свойства напрямую, не пересекаясь. Он ведет себя скорее как Omit-и-пересечение. Вот почему вы не можете использовать прямой перекресток. Если в вашем классе есть частные члены, вы не можете использовать Omit или любой сопоставленный тип, это недостающая функция согласно ms/TS#35416. Но вы все равно можете создать подкласс. Итак, вы либо пишете T & {user_data: U} и имеете дело с пересечением, либо создаете подкласс Foo. Это полностью решает вопрос? Если да, то напишу ответ, иначе чего не хватает?

jcalz 25.08.2024 21:48

@jcalz Я думаю, что создание подклассов или расширение интерфейса — единственный способ сузить это свойство, и вы не можете расширить общий аргумент, такой как interface MyFoo<B> extends B .... Спасибо за разъяснение, если вы хотите оставить это в качестве ответа, я приму это.

Levi Haskell 26.08.2024 03:55
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
11
63
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ваша последняя попытка пока самая близкая, но функция doSomething мешает сделать что-либо. Могу ли я предложить следующее:

declare class Foo<U = Record<string, any>> {
    readonly user_data: U

    // other members including private
    private __secret(): void;
}

declare function doSomething<U>(foo: Foo<U>): void;

declare const foo: Foo<{
    a: string,
    b: number,
}>;

doSomething(foo);

Этот подход упрощает ситуацию за счет того, что функция doSomething становится универсальной. Однако он по-прежнему работает, поскольку Foo имеет тип по умолчанию для user_data.

Тай. Но проблема в том, что я не могу изменить doSomething или что-нибудь еще во внешнем .d.ts определении.

Levi Haskell 25.08.2024 20:54
Ответ принят как подходящий

Идеальным решением здесь было бы, если бы восходящий поток Foo использовал тип объекта или даже пустой тип объекта {} вместо Record<string, any>. Тип Record<string, any> эквивалентен {[k: string]: any}, строке индексной подписи, где каждое свойство имеет тип any и, следовательно, позволяет индексировать его с помощью любого ключа и делать что-либо с полученным свойством. Это означает, что он невероятно либерален и небезопасен по типу.

Кроме того, если вы хотите сузить этот тип, было бы хорошо, если бы восходящий Foo был универсальным в типе T свойства user_data. Этот тип может быть ограничен до object и даже по умолчанию до object, например:

declare class Foo<T extends object = object> {
    readonly user_data: T
    private __secret(): void;
}
        
declare function doSomething(foo: Foo): void;

declare const foo: Foo<{ a: string, b: number }>;
foo.user_data.a = "hello"; // ok
foo.user_data.a = 1; // error
foo.user_data.c = "hello"; // error
doSomething(foo); // ok

Но, очевидно, вы не можете изменить объявления типов восходящего потока Foo и не желаете создавать его локально. Итак, вы ищете другой подход.


Если class имеет частные или защищенные члены и вы хотите сузить одно из его свойств, не «видя» тип свойства из базового класса, то единственный работоспособный вариант — использовать наследование через расширения либо как интерфейс или как объявление подкласса. То есть, учитывая

declare class Foo {
    readonly user_data: Record<string, any>
    private __secret(): void;
}
declare function doSomething(foo: Foo): void;

ты можешь написать

interface MyFoo<T extends object> extends Foo {
    readonly user_data: T
}

а затем используйте MyFoo<T> вместо Foo там, где это необходимо:

declare const foo: MyFoo<{ a: string, b: number }>;
foo.user_data.a = "hello"; // ok
foo.user_data.a = 1; // error
foo.user_data.c = "hello"; // error
doSomething(foo); // ok

Другие попытки добиться такого поведения сталкиваются с различными проблемами.

Расширение класса/интерфейса, которое сужает свойства, ведет себя аналогично пересечению части с суженными свойствами с частью базового класса без этих свойств, поэтому interface X extends Y { z: Z } аналогично type X = Omit<Y, "z"> & { z: Z } использованию типа утилиты Omit . Но вы не можете сохранить свойства private или protected с помощью Omit или любого сопоставленного типа. По адресу microsoft/TypeScript#35416 есть запрос на открытие функции, позволяющий это сделать, но это не является частью языка. На данный момент единственный способ сохранить эти свойства — это наследование.

Часто пересечение все же может сойти с рук, если предположить, что type X = Y & {z: Z} ведет себя как Omit<Y, "z"> & {z: Z}, при условии, что если Y имеет свойство z, то результирующее свойство z типа Y["z"] & Z будет иметь тип, эквивалентный Z, и вы можете игнорировать Y["z"] тип. Если Z является сужением Y["z"], то оно должно быть эквивалентным, верно? Верно? Ну, часто это: string & "a"is just"a", and {} & {p: string}is just{p: string}. But sometimes it isn't completely equivalent. And since Record<string, Any>is so permissive and not type safe, you can't ignore it when intersecting withZ. The type Record<string, Any> & Zis going to look a lot likeRecord <строка, любой>`.

Так что если Omit<Y, "z"> & {z: Z} не работает, и Y & {z: Z} не работает, то нужно по сути отказаться от перекрестков. Вам нужно расширение интерфейса/класса. А поскольку для такого расширения требуются статически известные ключи, вы не можете написать полностью повторно используемый вспомогательный тип, например interface HUD<C, T> extends C {user_data: T}, где C — параметр универсального типа. Вам нужно написать interface MyFoo<T> extends Foo {user_data: T} и interface MyBar<T> extends Bar {user_data: T} и т. д.

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

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

Спасибо! Очень тщательный анализ. Я также передам его вышестоящим сопровождающим фреймворка.

Levi Haskell 26.08.2024 08:10

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