Напишите функцию, которая использует типы поиска в дженериках.

Я хочу написать функцию, которая делает три вещи:

  • работает с универсальным типом T
  • принимает ключ: K из T, где T[K] должен быть логическим значением
  • присваивает значение T[K]

Я следую этому руководству, которое подсказывает при такой возможности:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

interface Part {
    id: number;
    name: string;
    subparts: Part[];
    updatePart(newName: string): void;
}

type T40 = FunctionPropertyNames<Part>;  // "updatePart"
type T41 = NonFunctionPropertyNames<Part>;  // "id" | "name" | "subparts"
type T42 = FunctionProperties<Part>;  // { updatePart(newName: string): void }
type T43 = NonFunctionProperties<Part>;  // { id: number, name: string, subparts: Part[] }

Однако кажется, что я могу достичь своей цели только в том случае, если моя функция работает с конкретными типами. Чтобы вычислить логические имена свойств T, я изменил первую строку из примера следующим образом:

type BoolPropNames<T> = { [K in keyof T]: T[K] extends boolean ? K : never }[keyof T];

Это работает, если я использую конкретный тип:

class Thing {
  isGreat: boolean;
}

function assignToThing(thing: Thing, key: BoolPropNames<Thing>) {
  thing[key] = false;
}

Но если я попытаюсь работать с дженериком, это не так:

function assign<T>(thing: T, key: BoolPropNames<T>) {
  thing[key] = false;
}

Когда я пытаюсь это сделать, TypeScript выдает следующую ошибку:

(parameter) key: { [K in keyof T]: T[K] extends boolean ? K : never; }[keyof T]
Type 'false' is not assignable to type 'T[{ [K in keyof T]: T[K] extends boolean ? K : never; }[keyof T]]'.ts(2322)

Почему TypeScript не позволяет мне работать с дженериками таким образом и что я могу сделать, чтобы это исправить?


person Samo    schedule 31.01.2020    source источник


Ответы (1)


TS не может вычислить результат BoolPropNames<T>, поскольку он вычисляется, когда задано T, иначе, чем в предыдущих примерах, где у вас не было переменной типа, а был какой-то конкретный тип. Например, BoolPropNames<Thing> вычисляется как isGreat, а thing[key] известно как boolean.

BoolPropNames<T> нельзя вычислить раньше, поэтому он не знает, что результатом будет ключ, дающий нам boolean. И если он не знает, что think[key] является логическим, вы не можете присвоить ему логическое значение.

Чтобы исправить это, мы можем, например, применить аргумент, который будет считаться типом T[K]. Тогда TS не нужно вычислять тип, так как он всегда будет значением типа по заданному ключу. Рассмотрим следующий код:

function assign<T, K extends BoolPropNames<T>>(thing: T, key: K, value: T[K]) {
  thing[key] = value;
}
assign({ a: true }, 'a', false);

Если третий аргумент вас не устраивает, и вы просто хотите установить любое значение, не указывая его в качестве аргумента, мы также можем сделать частичное приложение, чтобы создать такое поведение. Рассмотреть возможность:

// function which creates assign function
const createAssign = <T, K extends BoolPropNames<T> = BoolPropNames<T>>
(value: T[K]) => (thing: T, key: K) => {
  thing[key] = value;
}
// below assign function is created for type `Example`
type Example = { a: boolean };
const assign = createAssign<Example>(false); 
assign({a: true}, 'a');

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

Конечно, недостатком является то, что нам нужно генерировать их для каждого типа, устанавливая переменную универсального типа, поэтому это не так полиморфно, как решение с другим порядком аргументов.

person Maciej Sikora    schedule 31.01.2020
comment
Но он знает, что ключ даст нам логическое значение: T[K] extends boolean ? K : never - person Roberto Zvjerković; 31.01.2020
comment
Он даже не вычисляет это, пока не задана переменная типа. Таким образом, он не анализирует определения этого типа на этом уровне. Так что не знает - person Maciej Sikora; 31.01.2020
comment
Суть в том, чтобы понять, как рассматривать BoolPropNames как функцию, не имеющую аннотации вывода. Это означает, что пока мы не выполним его, пока мы не знаем, что он вернет. TS не вычисляет это, и это видно также по ошибке, которую вы видите, TS просто дает вам полное определение, поскольку он не знает, каким оно будет в конечном итоге. - person Maciej Sikora; 31.01.2020
comment
Спасибо за предложения. Хотя мне кажется, что TS должен предполагать, что он будет работать для любого T, у которого есть булевы свойства! Он не должен жаловаться, если только я не вызову assign с T и K, где T[K] не является логическим значением. - person Samo; 01.02.2020