Использование машинописного текста
как я могу сделать параметр «значение» в функции «formatValue» типом keyOf T
?
export interface ICol<T extends Record<string, any>, K extends keyof T = keyof T> {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
}
const col: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'a',
formatValue: (value) => {
return `Value is ${value}`;
},
};
Во всплывающей подсказке должно быть указано value:number
, когда метод доступа равен «a», но value:string
, когда метод доступа равен «b». Текущая подсказка гласит: (parameter) value: string | number | date
В этом случае мне нужно, чтобы значение было числом.
Спасибо.
потому что, если нет formatValue.. я просто напечатаю значение как есть!
• Внесите изменения, чтобы включить информацию о всплывающей подсказке в виде обычного текста. • Является ли этот подход тем, что вы ищете? Если да, то я напишу ответ или найду подходящий дубликат. Если нет, то что мне не хватает?
ух ты! решение работает!.. я пытаюсь это понять! это потрясающий человек! УХ ТЫ! пожалуйста, напишите ответ, чтобы одобрить его.
Мне интересно, можно ли решить эту проблему, просто используя типы, или вместо этого можно использовать интерфейс. Я знаю, что использование типа работает, но, возможно, я слишком усложняю ситуацию, пытаясь заставить его работать с интерфейсом, а может быть, это даже невозможно.
Нет, вы не можете использовать interface
здесь, потому что мы фактически определяем тип объединения. Итак, это будет type MyUnion = 'foo' | 'bar'
, который вы не можете определить с помощью interface
.
Это отличный вопрос, и чтобы он сработал, требуется некоторая хитрость. Окончательное решение будет выглядеть примерно так...
export type $Values<T extends object> = T[keyof T];
export type ICol<T extends Record<string, any>> = $Values<{
[K in keyof T]: {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
}
}>
const colA: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'a',
formatValue: (value) => {
return `Value is ${value.toFixed()}`; // value is a number
},
};
const colB: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'b',
formatValue: (value) => {
return `Value is ${value.charCodeAt(1)}`; // value is a string
},
};
const colC: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'c',
formatValue: (value) => {
return `Value is ${value.getDate()}`; // value is a Date
},
};
Так что же это делает? Идея состоит в том, что мы хотим получить результирующий тип, представляющий собой объединение всех возможных вариантов, который будет выглядеть примерно так...
type FinalType =
| {
accessor?: 'a';
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
}
| {
accessor?: 'b';
formatValue?: ((value: string) => JSX.Element | string | number) | undefined;
}
| {
accessor?: 'c';
formatValue?: ((value: Date) => JSX.Element | string | number) | undefined;
};
Для этого мы перебираем все ключи объекта T
, используя синтаксис [K in keyof T]
. Это даст нам тип, который выглядит так...
type LoopedObjectTypes = {
a: {
accessor?: 'a';
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
};
b: {
accessor?: 'b';
formatValue?: ((value: string) => JSX.Element | string | number) | undefined;
};
b: {
accessor?: 'c';
formatValue?: ((value: Date) => JSX.Element | string | number) | undefined;
};
}
Затем мы просто переносим значения по объекту, используя вспомогательный тип $Values
, и получаем окончательный тип.
Одна вещь, которую я настоятельно рекомендую при попытке понять такой сложный тип, — это обратиться к этому репозиторию Utility-Types, который содержит множество полезных типов для использования в вашем коде, а также позволяет увидеть, как эти типы на самом деле создаются.
Вы хотите, чтобы ICol<{ a: number; b: string; c: Date; }>
без второго аргумента общего типа K
был эквивалентен объединению ICol<{ a: number; b: string; c: Date; }, K>
для каждого K
в keyof T
. То есть вы хотите распределить объединение в K
по операции ICol<T>
. Есть два относительно простых способа сделать это.
Один из них — использовать распределительный условный тип , например:
type ICol<T extends Record<string, any>, K extends keyof T = keyof T> =
K extends unknown ? {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
} : never
Поскольку K
является параметром универсального типа, условный тип K extends unknown ? ⋯ : never
автоматически распределяет ⋯
по объединениям в K
. На самом деле мы не пытаемся ничего проверять (K extends unknown
всегда верно), просто используем дистрибутивный условный тип. Это приводит к:
type Z = ICol<{ a: number; b: string; c: Date; }>;
/* type Z = {
accessor?: "a" | undefined;
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
} | {
accessor?: "b" | undefined;
formatValue?: ((value: string) => JSX.Element | string | number) | undefined;
} | {
accessor?: "c" | undefined;
formatValue?: ((value: Date) => JSX.Element | string | number) | undefined;
} */
И тогда вы получаете ожидаемое поведение, где value
выводится как number
:
const col: ICol<{ a: number; b: string; c: Date; }> = {
accessor: 'a',
formatValue: (value) => {
return `Value is ${value}`;
},
};
Другой способ — использовать тип распространяемого объекта, как это описано в microsoft/TypeScript#47109 , где вы создаете сопоставленный тип над членами объединения K
, а затем немедленно индексируете в этот сопоставленный тип с помощью K
:
type ICol<T extends Record<string, any>, K extends keyof T = keyof T> = { [P in K]: {
accessor?: P;
formatValue?: (value: T[P]) => JSX.Element | string | number;
} }[K]
Здесь мы объявляем параметр типа P
для перебора элементов K
. В результате создается сопоставленный тип с теми же ключами, что и T
, но свойства которого являются типами объектов, которые мы ищем. Когда мы индексируем этот сопоставленный тип с помощью K
, мы получаем объединение этих типов объектов.
Таким образом, происходит тот же вывод:
type Z = ICol<{ a: number; b: string; c: Date; }>;
/* type Z = {
accessor?: "a" | undefined;
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
} | {
accessor?: "b" | undefined;
formatValue?: ((value: string) => JSX.Element | string | number) | undefined;
} | {
accessor?: "c" | undefined;
formatValue?: ((value: Date) => JSX.Element | string | number) | undefined;
} */
Эти типы по-прежнему позволяют вам передать тип для K
, если вы не хотите использовать по умолчаниюkeyof T
, поэтому вы можете получить более конкретный тип, если хотите:
type X = ICol<{ a: number; b: string; c: Date; }, "a">;
/* type X = {
accessor?: "a" | undefined;
formatValue?: ((value: number) => JSX.Element | string | number) | undefined;
} */
Если вам не нужна эта возможность, вы можете переписать ICol<T>
, чтобы удалить второй параметр типа:
type ICol<T extends Record<string, any>> =
keyof T extends infer K ? K extends keyof T ? {
accessor?: K;
formatValue?: (value: T[K]) => JSX.Element | string | number;
} : never : never;
или
type ICol<T extends Record<string, any>> = { [P in keyof T]: {
accessor?: P;
formatValue?: (value: T[P]) => JSX.Element | string | number;
} }[keyof T]
Обратите внимание: поскольку ICol<T>
хотя бы иногда должен быть типом объединения, вы не можете создать ICol<T>
интерфейс 🔁 . Интерфейсы могут представлять только отдельные типы объектов (или пересечения типов объектов) со статически известными членами.
Почему
formatValue
является необязательной функцией-членом? Это не самый удобный дизайн API...