Обобщения во вложенной функции TypeScript

#typescript #typescript-generics

#typescript #typescript-обобщения

Вопрос:

Я хочу написать функцию TypeScript, которая принимает только определенные строки, которые являются именами свойств указанного типа, но эти свойства должны иметь тип string , number или Date . Эта функция возвращает другую функцию, которая принимает исходный объект (для которого были выбраны имена свойств) и возвращает указанное свойство (на самом деле это делает что-то еще, но это простейший случай, который вызывает ту же проблему)

Я попробовал это так

 "use strict";
type InstancePropertyNames<T, I> = {
    [K in keyof T]: T[K] extends I ? K : never;
}[keyof T];
type StringPropertyNames<T> = InstancePropertyNames<T, string>;
type NumberPropertyNames<T> = InstancePropertyNames<T, number>;

function doSomething<T extends object>(
    key:
        | StringPropertyNames<T>
        | NumberPropertyNames<T>
        | InstancePropertyNames<T, Date>
) {
    return function(obj: T) {
        const res = obj[key];
        return res;
    }
}
function doSomethingEasy<T extends object>(
    obj: T,
    key:
        | StringPropertyNames<T>
        | NumberPropertyNames<T>
        | InstancePropertyNames<T, Date>
) {
    const res = obj[key];
    return res;
}

type Test = {
    asdf: string;
    foo: number;
    bar: boolean;
    qux: Date;
};

const test: Test = {
    asdf: "asdf",
    foo: 42,
    bar: true,
    qux: new Date()
}

const res = doSomething<Test>("asdf")(test);
console.log(res);
const resEasy = doSomethingEasy(test, "asdf");
console.log(resEasy);
  

Игровая площадка TypeScript

Теперь проблема в том, что тип res внутри вложенной функции является чем-то сложным (вместо простого number | string | Date ), также в doSomethingEasy . Есть ли какой-либо способ упростить тип свойства во вложенной функции до примитивных типов?

Ответ №1:

Ваша doSomething() функция curried, в которой вы не знаете, какой тип T должен быть, пока не вызовете функцию, возвращаемую doSomething() . Это делает практически невозможным для компилятора вывод T . Чтобы сделать это очевидным, представьте это:

 const f = doSomething("asdf"); // what type should f be?
f({asdf: 123});
f({asdf: "a"});
f(test);
  

Параметр T выводится компилятором при вызове doSomething("asdf") функции produce f . Но что это должно быть выведено? Должно ли это зависеть от того, что вы вызываете f() с помощью? А что, если вы вызываете f() с разными типами, как я сделал выше? Единственное, что здесь могло бы сработать, — это if f сама по себе является универсальной функцией, но компилятор не знает достаточно, чтобы попытаться вывести это.

Таким образом, вы получаете T вывод как что-то вроде {asdf: any} (потому что обратный вывод через условные типы like InstancePropertyNames также практически невозможен), а затем f() выдает any все, с чем вы это называете. Упс.


Вместо этого я бы попытался написать вашу подпись так, чтобы на каждом шаге компилятору нужно было знать только параметры функции типа a, чтобы вывести ее параметры типа. Что-то вроде этого:

 function doSomething<K extends PropertyKey>(
    key: K
) {
    return function <T extends Record<K, string | number | Date>>(obj: T) {
        const res = obj[key];
        return res;
    }
}
  

Здесь doSomething будет принят key тип K , который может быть любым значением, подобным ключу. Поскольку key определяет K , вы можете предположить, что K это будет выведено правильно. Мы не создаем doSomething() generic, T поскольку ничто, переданное в doSomething() , не поможет определить, что T должно быть.

Возвращаемое значение doSomething() — это еще одна универсальная функция, которая принимает obj тип T of, который ограничен Record<K, string | number | Date> . Итак, мы вставили T generic в сигнатуру для возвращаемой функции, которая на самом деле должна знать достаточно, чтобы вывести ее.

(Эта возвращаемая функция не обязательно должна быть generic in T ; вы могли бы ввести type obj as Record<K, string | number | Date> напрямую, но тогда срабатывают избыточные проверки свойств там, где они могут вам не понадобиться, а также возвращаемое значение возвращаемой функции всегда string | number | Date будет шире, чем вы могли бы хотеть. Преимущества сохранения T ниже:)

Поскольку K это уже известно, это должно означать, что T это будет выведено как тип obj , и он будет принимать только obj параметры, значение которых в K ключе можно присвоить string , number , или Date . И возвращаемый тип возвращаемой функции будет T[K] , тип свойства T at key K .

Давайте посмотрим, работает ли это:

 const f = doSomething("asdf");
// const f: <T extends Record<"asdf", string | number | Date>>(obj: T) => T["asdf"]

const res = doSomething("asdf")(test); // string
console.log(res); // "asdf"

console.log(
  doSomething("bar")({foo: true, bar: new Date(), baz: 123}).getFullYear()
); // 2020
  

Выглядит хорошо. Вызов doSomething("asdf") возвращает обобщенную функцию, принимающую значения, которым можно присваивать {asdf: string | number | Date} . И так doSomething("asdf")(test) выдает значение типа string . Вызов в конце показывает универсальность этой реализации; вы получаете a Date out, потому что компилятор знает, что bar свойство переданного объектного литерала равно a Date . (Вот почему полезно иметь оба K и T быть универсальным. В противном Record<K, string | number | date>[K] случае это просто string | number | date . Тип T[K] более узкий и, вероятно, более полезный для вас.)

Игровая площадка ссылка на код

Комментарии:

1. Спасибо за подробный ответ! Поскольку вы ответили так быстро, я не знаю, получили ли вы мою правку через 2 минуты после первоначального сообщения (она не видна). Я понял, что забыл передать тип в качестве параметра, для doSomething которого правильно разрешен возвращаемый тип, но в функции тип свойства довольно сложный, где он просто может быть number | string | Date . Можно ли это упростить? (кроме мошенничества с prop as unknown as string | number | Date )

2. А, понятно. Хорошо, я посмотрю, когда доберусь до реального компьютера, предполагая, что пока никто не ответит.

3.Хорошо, хорошо: нет, компилятор недостаточно умен, чтобы обобщенно рассуждать о том, что T[IPN<T, string>] | T[IPN<T, number>] | T[IPNames<T, Date>] будет присваиваться string | number | Date (не равно этому, потому что, например, тип {a: number} должен выдавать res тип типа number ). Для конкретного T он может фактически оценить его, но не для неопределенного дженерика T . Если вы хотите, чтобы это произошло внутри реализации, вы должны использовать утверждение типа …. или вы могли бы использовать решение, которое я даю в этом ответе, которое понимает компилятор.

4. Моим предлагаемым решением по-прежнему будет код в этом ответе, поскольку компилятору проще сделать вывод и обработать. Если по какой-то причине вам действительно нравится или нужно указывать T , когда вы вызываете doSomething() , и сложные условные InstancePropertyNames ограничения, тогда вы получите что-то сложное, и вам нужно будет использовать утверждения типа или тому подобное, чтобы справиться с этим. Надеюсь, это поможет. Удачи!

5. Спасибо за ваши предложения! Я пробовал это с утверждениями типа, но, например res as number , внутри функции выдает ошибку, что типы T[symbol] и number несовместимы… Я попробую это с вашим подходом дальше!