Ограничить один общий параметр Typescript на основе свойств другого?

Я пытаюсь написать функцию, которая принимает объект и (строковый) ключ, а затем оперирует свойством объекта. Это легко:

function f<T extends any, K extends keyof T>(obj: T, key: K) {
  const prop = obj[key]; // prop is typed as T[K]
}

Я хотел бы ограничить ключ, передаваемый вызову, во время компиляции, в зависимости от типа T[K]. Я пробовал это:

function f<T extends any, K extends keyof T>(obj: T, key: T[K] extends number ? K : never) {
  obj[key] = 5; // error, "is not assignable to" etc
}

prop набирается как T[T[K] extends number ? K : never], что для меня читается так, как будто он должен свернуться до number, но это не так.

Моя цель - убедиться, что obj[key] набирается как number внутри функции, а также чтобы такие вызовы, как f({a: true}, "a"), были помечены как ошибка. Это возможно? Я думал, что мне может потребоваться переместить ограничение из объявления параметра функции в объявление общего параметра, но я не мог понять синтаксис.


ETA еще раз: Пример игровой площадки - обновлено, чтобы попробовать подход, предложенный в комментарии, предложенном в @

type AssignableKeys<T, ValueType> = {
  [Key in keyof T]-?: ValueType extends T[Key] | undefined ? Key : never
}[keyof T];

type PickAssignable<T, ValueType> = Pick<T, AssignableKeys<T, ValueType>>;

type OnlyAssignable<T, ValueType> = {
  [Key in AssignableKeys<T, ValueType>]: ValueType
};

interface Foo {
  a: number;
  b: string;
  nine: 9;
  whatevs: any;
}

type FooNumberKeys = AssignableKeys<Foo, number>; // "a" | "whatevs"
type CanAssignNumber = PickAssignable<Foo, number>; //  { a: number; whatevs: any; }
type DefinitelyJustNumbers = OnlyAssignable<Foo, number>; //  { a: number; whatevs: number; }

function f1<T>(obj: OnlyAssignable<T, number>, key: keyof OnlyAssignable<T, number>) {
  obj[key] = Math.random(); // Assignment is typed correctly, good
}

function f2<T extends object, K extends keyof PickAssignable<T, number>>(obj: T, key: K) {
  obj[key] = Math.random(); // Uh oh, Type 'number' is not assignable to type 'T[K]'.(2322)
}

declare const foo: Foo;
f1(foo, "a"); // Allowed, good
f1(foo, "whatevs"); // Allowed, good
f1(foo, "nine"); // Uh oh, should error, but doesn't!
f1(foo, "b"); // Error, good

f2(foo, "a"); // Allowed, good
f2(foo, "whatevs"); // Allowed, good
f2(foo, "nine"); // Error, good
f2(foo, "b"); // Error, good

На игровой площадке DefinitelyJustNumbers показывает всплывающую подсказку с {a: number; whatevs: number} - все, что я могу назначить number, явно набирается как number. Это фиксирует присвоение внутри тела функции, но не позволяет обнаружить тот факт, что nine - это только подмножество числа и поэтому не должно быть разрешено.

CanAssignNumber показывает всплывающую подсказку с {a: number; whatevs: any}, правильно исключая nine, потому что он не может быть назначен number. Выглядит неплохо, но по-прежнему не исправляет присвоение внутри функции f2.


person Coderer    schedule 20.10.2020    source источник
comment
Отвечает ли это на ваш вопрос? Как сделать Мне нужен ключ для свойства определенного типа?   -  person Reactgular    schedule 22.10.2020
comment
@Reactgular Я обновил свой пост, чтобы решить эту проблему, но я забыл, что редактирование сообщения, вероятно, не уведомило вас. Итак: пинг!   -  person Coderer    schedule 23.10.2020


Ответы (2)


Просто используйте утверждение типа, ваша проблема должна быть связана с сайтом вызова, который набран правильно и выдает ошибки там, где это должно быть. Реализация не может быть правильно напечатана, если вы хотите присвоить определенное значение внутри функции.

Вы можете сделать результат T[K] расширенным числом, например, но добавив ограничение к T из Record<K, number>, но мы по-прежнему не сможем присвоить конкретные значения obj[key]

type KeyOfType<T, ValueType> = 
  { [Key in keyof T]-?: T[Key] extends ValueType | undefined ? Key : never }[keyof T]

function f<T extends Record<K, number>, K extends KeyOfType<T, number>>(obj: T, key: K, value: T[K]) {
    let r = obj[key]; 
    r.toExponential(); // seems numberish, but it `T[K]` which does extend number, but might not be number
    obj[key] = obj[key] // T[K] is assignable to T[K]
    obj[key] = value; // even if it is a parameter 
    obj[key] = 5; // still an error
}

declare const foo: Foo;
f(foo, "a", 1); // Allowed, good
f(foo, "b", 2); // Error, good

const other: { 
  a: 1
} = {
  a: 1
}
f(other, "a", 1) // this will break the type of other because of obj[key] = 5

площадка Link

Причина этого - последний пример f(other, "a", 1). Здесь a в other имеет тип 1, который расширяет number, поэтому f(other, "a", 1) является допустимым вызовом f, но внутри мы хотим назначить other[key] = 5. Это сломало бы тип other. Проблема здесь в том, что нет способа указать, что T[K] имеет верхнее ограничение number только нижнее ограничение.

person Titian Cernicova-Dragomir    schedule 21.10.2020
comment
Первый: типу ввода T нужно разрешить иметь свойства, отличные от number, поэтому Record бесполезен. Два: мне нужно верхнее ограничение для ключа K, чтобы некоторый тип (назовем его V) мог быть назначен T[K]. Я не понимаю, почему это ограничение невозможно выразить. Я обновил вопрос, добавив новый пример лучшей (но все же неудачной) попытки Playground. - person Coderer; 21.10.2020
comment
@Coderer В системе типов нет поддержки для верхних границ. Когда вы выражаете ограничение, такое как T extends number, T может быть числом, а также любым подтипом number, который может быть любым числовым буквальным типом, таким как 1, 2 и т. Д. Вы можете заставить компилятор понять это T[K] extends number, но только означает, что T[K] должен быть подтипом number, не обязательно number. Таким образом, присвоение obj[key] = 1 не является правильным, потому что obj[key] может иметь тип number, 1 или 2, и для одного из них назначение было бы неверным. - person Titian Cernicova-Dragomir; 21.10.2020
comment
Однако в приведенном выше переписывании у меня есть number extends T или что-то в этом роде. Конечно, это возможно только в r-значении сопоставленного типа, а не в общем ограничении, но сопоставленный тип в моем последнем обновлении вопроса действительно соответствует ключам, которые я хотел, поэтому я надеялся, что это как бы просочится. Похоже, мне все еще нужно утверждение, но после внесенных мною изменений я не могу придумать контрпример, показывающий, почему это неправильно / опасно. - person Coderer; 21.10.2020

Вы должны расширяться от ключей, которые приводят к значению типа number.

export type PickByValue<T, ValueType> = Pick<
  T,
  { [Key in keyof T]-?: T[Key] extends ValueType ? Key : never }[keyof T]
>;

function f<T extends object, K extends keyof PickByValue<T, number>>(obj: T, key: K) : T[K] {
  return obj[key]
}

Изменить: То, что вы пытаетесь сделать, невозможно в TS AFAIK, и иногда для этого есть веская причина. предположим, что у вас есть код ниже:

function f<T extends object, K extends keyof PickByValue<T, number>>(obj: T, key: K) {
    obj[key] = 5; // Type 'number' is not assignable to type 'T[K]'.
} 

const obj = {a: 9} as const;

f(obj, "a")

Например, в приведенном выше сценарии значение свойства a является числом, но не типа number, а типа 9. Машинописный текст не знает об этом заранее. в других сценариях единственное, что мне приходит в голову, это использование Введите утверждения.

person Mahdi Ghajary    schedule 20.10.2020
comment
Это полезный способ выразить то, что я пытаюсь сделать, спасибо! К сожалению, фактически не работает. - person Coderer; 21.10.2020
comment
Я обновил вопрос более подробно. Слишком поздно редактировать приведенный выше комментарий, извините. - person Coderer; 21.10.2020
comment
Согласно приведенному ниже ответу Тициана, я хочу установить верхнее ограничение (верхняя граница?) Для типа T[K], чтобы тип ограничения (number в примере) всегда мог быть назначен T[K]. Обновленный пример в моем вопросе поймает ваш контрпример и пометит его как недействительный. (Однако я все еще не могу получить возможность назначения для работы в теле функции.) - person Coderer; 21.10.2020
comment
@Coderer Я действительно не могу придумать способ добиться того, чего вы хотите достичь. с нынешней мощностью TS, я не думаю, что вы можете сделать лучше, чем это. Было бы неплохо спросить, зачем вам вообще нужно переназначать свой ввод, обычно есть более эффективные способы. - person Mahdi Ghajary; 22.10.2020