Использование машинописного текста
как я могу сделать параметр «значение» в функции «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...