Я хочу создать функцию, которая может выполнять сужение типа для объединения, которое действует как размеченное объединение во время выполнения, но не относится к типу размеченного объединения в TypeScript.
В настоящее время у меня есть этот (упрощенный) код, но props внутри операторов if всегда имеет тип WidgetPropsSet:
if (props.kind === "CheckboxInput") {
console.info("We need to use", props, "here as its narrowed `CheckboxInput` type");
}
if (props.kind === "DropdownInput") {
console.info("We need to use", props, "here as its narrowed `DropdownInput` type");
}
// ... 16 more
(неупрощенная версия этого кода является шаблоном Svelte, поэтому структура, показанная выше, не может быть изменена)
Я думаю, что мог бы заменить props.kind === "discriminator" внутри оператора if функцией (псевдокод TS):
function narrowWidgetProps(props: WidgetPropsSet, kind: WidgetPropsNames): a-concrete-variant-from-WidgetPropsSet | undefined {
if (props.kind === kind) return a-concrete-variant-from-WidgetPropsSet;
else return undefined;
}
Как мне написать эту функцию псевдокода в правильном TypeScript, который может выполнить необходимое сужение?
Для контекста это варианты союза, с которыми я имею дело:
abstract class WidgetProps {
kind!: string;
}
class CheckboxInput extends WidgetProps {
checked!: boolean;
disabled!: boolean;
// ...
}
class DropdownInput extends WidgetProps {
entries!: string[];
selectedIndex!: number | undefined;
// ...
}
И вот фактическое объединение, которое должно быть в этом формате { value, name }[], чтобы widgetSubTypes можно было передать стороннему коду, которому он нужен в этом формате.
const widgetSubTypes = [
{ value: CheckboxInput, name: "CheckboxInput" },
{ value: DropdownInput, name: "DropdownInput" },
// ...
] as const;
// Evaluated type:
// const widgetSubTypes: readonly [{
// readonly value: typeof CheckboxInput;
// readonly name: "CheckboxInput";
// }, {
// readonly value: typeof DropdownInput;
// readonly name: "DropdownInput";
// }, ... 16 more ..., {
// ...;
// }]
type WidgetPropsSet = InstanceType<typeof widgetSubTypes[number]["value"]>;
// Evaluated type:
// type WidgetPropsSet = CheckboxInput | DropdownInput | ... 16 more
type WidgetPropsNames = typeof widgetSubTypes[number]["name"];
// Evaluated type:
// type WidgetPropsNames = "CheckboxInput" | "DropdownInput" | ... 16 more
Вот ссылка на TypeScript Playground, где единственную функцию вверху можно отредактировать, чтобы решить этот вопрос.
Я в замешательстве; props не относится к типу размеченных союзов; не должны ли CheckboxInput и DropdownInput иметь строго типизированные kind свойства, как показано здесь? Если нет, то вопрос, кажется, не о дискриминированном союзе.
Извините за это @jcalz, я обновил ссылку на игровую площадку в основном посте этой важной строкой, которую я забыл, которая теперь правильно демонстрирует упрощенную версию случая, с которым я столкнулся: const props = Math.random() > 0.5 ? checkboxInputInstance : dropdownInputInstance; Это проясняет ваше замешательство?
Хорошо, теперь это объединение, но это все еще не размеченное объединение, которое является особой структурой данных, которую TS поддерживает для сужения. В вашем союзе нет дискриминационной собственности; kind это просто string. Предположительно, во время выполнения вы убедитесь, что kind можно использовать для различения членов объединения, но это не закодировано в системе типов.
Если я попытаюсь проигнорировать слова «дискриминационный союз» и буду продвигаться вперед, я получу вот такой подход. Это соответствует вашим потребностям? Если это так, я мог бы написать ответ с объяснением (хотя было бы неплохо, если бы мы заменили «дискриминированный союз» каким-то другим термином). Если нет, то что мне не хватает?
Спасибо @jcalz, это действительно решает проблему как на игровой площадке, так и в моем реальном коде. Вы правы, кажется немного странным называть это размеченным союзом, но я не знал, как еще его назвать, потому что он связан с этой концепцией, а не с хрестоматийным примером его синтаксиса. Я был бы рад отредактировать вопрос (или вы можете) до того, как вы ответите, чтобы уточнить его значение и, надеюсь, улучшить результаты поиска, чтобы принести пользу другим будущим посетителям. Пожалуйста, дайте мне ваши предложения о том, как лучше всего описать любые неточности в вопросе.
Давайте продолжим обсуждение в чате.
Давайте продолжим обсуждение в чате.
В идеале члены union должны иметь литеральные типизированные kind свойства, так что это будет настоящий размеченный союз, и вы получите желаемое сужение автоматически, не нуждаясь во вспомогательной функции:
class CheckboxInput extends WidgetProps {
declare checked: boolean;
declare disabled: boolean;
declare kind: "CheckboxInput" // <-- literal type
}
class DropdownInput extends WidgetProps {
declare entries: string[];
declare selectedIndex: number | undefined;
declare kind: "DropdownInput" // <-- literal type
}
declare const props: CheckboxInput | DropdownInput;
if (props.kind === "CheckboxInput") {
props.checked // props is automatically narrowed to CheckboxInput
}
if (props.kind === "DropdownInput") {
props.entries // props is automatically narrowed to DropdownInput
}
Но если по какой-то причине вы не можете этого сделать и вам нужно оставить тип свойства kind в каждом подклассе, то мы можем написать вашу функцию narrowWidgetProps() следующим образом:
const widgetSubTypes = [
{ value: CheckboxInput, name: "CheckboxInput" },
{ value: DropdownInput, name: "DropdownInput" },
] as const;
type WidgetSubTypes = typeof widgetSubTypes[number];
type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> };
/* type WidgetKindMap = {
CheckboxInput: CheckboxInput;
DropdownInput: DropdownInput;
} */
type WidgetPropsNames = keyof WidgetKindMap
type WidgetPropsSet = WidgetKindMap[WidgetPropsNames]
function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) {
if (props.kind === kind) return props as WidgetKindMap[K]
else return undefined;
}
Я использую определение widgetSubTypes для генерации WidgetKindMap, вспомогательного типа, ключами которого являются буквальные значения kind (которые вместе составляют объединение WidgetPropsNames), а значениями являются типы экземпляров соответствующих подклассов WidgetProps (которые вместе составляют WidgetPropsSet союз).
Тогда функция narrowWidgetProps() является общей в K, типом параметра kind. Если это не соответствует свойству kind аргумента props, мы возвращаем undefined. Если он действительно соответствует, то мы утверждаем , что props на самом деле относится к типу WidgetKindMap[K] (индексированный тип доступа означает «тип значения WidgetKindMap, соответствующий ключу типа K»).
Нам нужно это утверждение, as WidgetKindMap[K], потому что у компилятора нет оснований полагать, что сопоставление kind свойств действительно может сузить props до этого типа. Если бы он действительно так думал, то WidgetPropsSet уже был бы дискриминированным союзом, и это усилие было бы ненужным, как упоминалось выше.
В любом случае, давайте проверим это:
declare const props: WidgetPropsSet;
const narrowedCheckboxInput = narrowWidgetProps(props, "CheckboxInput");
if (narrowedCheckboxInput) {
// const narrowedCheckboxInput: CheckboxInput | undefined
narrowedCheckboxInput.checked
}
const narrowedDropdownInput = narrowWidgetProps(props, "DropdownInput");
if (narrowedDropdownInput) {
// const narrowedDropdownInput: DropdownInput | undefined
narrowedDropdownInput.entries
}
Выглядит неплохо. Тип возвращаемого значения narrowWidgetProps() зависит от аргумента kind так, как вы хотите.
Площадка ссылка на код
@jcalz Код, который я предоставил, уже завершен (за исключением одного нерелевантного типа в примере class DropdownInput, который я сейчас изменил на string[]). И я думаю, что это относительно минимально, не жертвуя слишком большим контекстом. Я обновил вопрос ссылкой на TS Playground, где функция, которую я хочу решить, легко редактируется. Спасибо :)