Я имею дело с внешне определенным типом и постоянной переменной, которая имеет общее свойство 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
также удаляет всех частных участников. Если бы был способ избежать этого, это решило бы мою проблему.
Пожалуйста, отредактируйте это, чтобы уточнить, в чем именно заключается ваш вопрос. Вы спрашиваете, почему Record<string, any> & {y: string}
позволяет вам получить доступ к z
собственности? Если да, то Record<string, any>
именно это и делает. Это позволяет вам получить доступ к любому свойству вообще. Возможно, вы хотите object
вместо Record<string, any>
. Или вы спрашиваете, как заставить Foo
и его подтипы действовать так, как вы хотите? Если да, то я бы посоветовал вам просто сделать Foo
общий, как показано по ссылке на эту игровую площадку . Или вы спрашиваете что-то другое? В любом случае, пожалуйста, отредактируйте, чтобы задать один четко сформулированный основной вопрос.
Foo
— это часть внешней структуры, которую я должен использовать, но не могу контролировать. Фреймворк имеет множество типов, которые следуют тому же шаблону, что и Foo
, т. е. предоставляют свойство user_data
, определенное как Record<string, any>
. Моя цель — сузить/ограничить тип user_data
, чтобы он мог иметь только те свойства, которые я хочу. И сделать это без объявления интерфейса, который каждый раз расширяет каждый конкретный Foo
.
В посте до сих пор нет ни одного четко определенного вопроса. Из заголовка "пересечение с типом записи" и "а зачем?" прокомментируйте, похоже, вы спрашиваете о поведении или Record<string, any> & {y: string}
и о подтипах Foo
и частных свойствах и Omit
- все это посторонние. Или, что более вероятно, вы действительно спрашиваете о подтипах Foo
, а все остальное — неудачные попытки этого добиться. Не могли бы вы отредактировать, чтобы удалить все постороннее в вашем вопросе и/или сделать так, чтобы читатель сразу понял, в чем заключается вопрос?
(см. предыдущий комментарий). Предполагая, что речь идет не только о пересечениях: вы не можете программно манипулировать типами классов с закрытыми/защищенными членами так, как вы хотите. Если вы не можете изменить Foo
, вы можете подклассифицировать его, как показано по этой ссылке на игровую площадку , а затем обратиться к общему подклассу. Таким образом, вы можете просто использовать аргумент универсального типа для типа подкласса. (И обратите внимание: подкласс может существовать только как тип в TS и на самом деле не обязательно должен существовать как конструктор). Я был бы рад написать такой ответ, если вопрос отредактировать, чтобы перестать спрашивать о перекрестках.
Дополнительные вопросы перефразированы. Моя цель, как я пытаюсь объяснить выше, состоит в том, чтобы: 1. Ограничить тип user_data
произвольного внешнего Foo
типа возможными непубличными членами 2. Сделать это без изменения внешних .d.ts
определений (как показано выше) 3. Избегать создавая подклассы для каждого конкретного Foo
и вместо этого используйте общий тип утилиты. Остальное — мои неудачные попытки достичь этого. Хотя я думаю, что они актуальны.
Если вы хотите написать ответ, возможно, вы захотите выяснить, почему создание подклассов работает для правильного ограничения типа свойства type user_data
, в то время как пересечение не может этого сделать.
Название по-прежнему «Пересечение с типом записи», поэтому я в замешательстве. Я могу написать ответ, но только тогда, когда я уверен, что понимаю вопрос и что мой потенциальный ответ касается его. Сейчас я все еще в замешательстве, извини.
Обновлен заголовок, надеюсь, это поможет. Тай
Создание подклассов и/или расширение интерфейса позволяет сузить свойства напрямую, не пересекаясь. Он ведет себя скорее как Omit
-и-пересечение. Вот почему вы не можете использовать прямой перекресток. Если в вашем классе есть частные члены, вы не можете использовать Omit
или любой сопоставленный тип, это недостающая функция согласно ms/TS#35416. Но вы все равно можете создать подкласс. Итак, вы либо пишете T & {user_data: U}
и имеете дело с пересечением, либо создаете подкласс Foo
. Это полностью решает вопрос? Если да, то напишу ответ, иначе чего не хватает?
@jcalz Я думаю, что создание подклассов или расширение интерфейса — единственный способ сузить это свойство, и вы не можете расширить общий аргумент, такой как interface MyFoo<B> extends B ...
. Спасибо за разъяснение, если вы хотите оставить это в качестве ответа, я приму это.
Ваша последняя попытка пока самая близкая, но функция 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
определении.
Идеальным решением здесь было бы, если бы восходящий поток 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 with
Z. The type
Record<string, Any> & Zis going to look a lot like
Record <строка, любой>`.
Так что если 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}
и т. д.
Это прискорбно, но вы не сможете сделать ничего лучше, не устраняя проблемы на предыдущем этапе.
Детская площадка, ссылка на код
Спасибо! Очень тщательный анализ. Я также передам его вышестоящим сопровождающим фреймворка.
У вас может быть единый интерфейс, в котором вы можете изменить общий тип, который вы назначите
user_data
, посмотрите, поможет ли это вам: Детская площадка