#typescript #generics
#typescript #дженерики
Вопрос:
Учитывая широко распространенный тип Omit, с определением:
type Omit<ObjectType, KeysType extends keyof ObjectType> =
Pick<ObjectType, Exclude<keyof ObjectType, KeysType>>;
который используется для вычитания типов (противоположных пересечению) или, другими словами, удаления определенных свойств из типа.
Я пытаюсь использовать этот тип для написания функции, которая примет объект типа T
с отсутствующим одним из свойств, затем установит это отсутствующее свойство и вернет значение типа T
.
Все в порядке со следующим примером, в котором используется определенный тип:
type A = { x: string }
function f(arg: Omit<A, 'x'>): A {
return { ...arg, x: "" }
}
но точно такая же функция, сделанная универсальной, не компилируется:
function g<T extends A>(arg: Omit<T, 'x'>): T {
return { ...arg, x: "" }
}
Ошибка в определении g
заключается в:
Тип ‘Pick> amp; { x: string; }’ нельзя назначить типу ‘T’.
Но я уверен, что это сообщение об ошибке неверно. Тип Pick<T, Exclude<keyof T, "x">> amp; { x: string; }
можно назначить T
.
Где я ошибаюсь?
Для большего контекста я пишу компонент React более высокого порядка, который будет принимать компонент и автоматически предоставлять некоторые из известных реквизитов, возвращая новый компонент с удаленными известными реквизитами.
Ответ №1:
ВНИМАНИЕ, ВПЕРЕДИ ДЛИННЫЙ ОТВЕТ. Краткие сведения:
-
Основная проблема известна, но может не получить должного внимания
-
Простое решение — использовать утверждение типа
return { ...arg, x: "" } as T;
-
Простое исправление не совсем безопасно и в некоторых крайних случаях приводит к плохим результатам
-
В любом случае,
g()
не выводитT
должным образом -
Переработанная
g()
функция внизу может быть лучше для вас -
Мне нужно перестать так много писать
Основная проблема здесь заключается в том, что компилятор просто недостаточно умен, чтобы проверить некоторые эквивалентности для универсальных типов.
// If you use CompilerKnowsTheseAreTheSame<T, U> and it compiles,
// then T and U are known to be mutually assignable by the compiler
// If you use CompilerKnowsTheseAreTheSame<T, U> and it gives an error,
// then T and U are NOT KNOWN to be mutually assignable by the compiler,
// even though they might be known to be so by a clever human being
type CompilerKnowsTheseAreTheSame<T extends U, U extends V, V=T> = T;
// The compiler knows that Picking all keys of T gives you T
type PickEverything<T extends object> =
CompilerKnowsTheseAreTheSame<T, Pick<T, keyof T>>; // okay
// The compiler *doesn't* know that Omitting no keys of T gives you T
type OmitNothing<T extends object> =
CompilerKnowsTheseAreTheSame<T, Omit<T, never>>; // nope!
// And the compiler *definitely* doesn't know that you can
// join the results of Pick and Omit on the same keys to get T
type PickAndOmit<T extends object, K extends keyof T> =
CompilerKnowsTheseAreTheSame<T, Pick<T, K> amp; Omit<T, K>>; // nope!
Почему это недостаточно умно? В общем, для этого есть два широких класса ответов:
-
Анализ типов, о котором идет речь, зависит от некоторой человеческой сообразительности, которую трудно или невозможно отразить в коде компилятора. Пока не произойдет сингулярность и компилятор TypeScript не станет полностью разумным, будут некоторые вещи, о которых вы можете рассуждать, но которые компилятор просто не может.
-
Анализ типов, о котором идет речь, относительно прост для выполнения компилятором. Но это займет некоторое время и, вероятно, окажет некоторое негативное влияние на производительность. Достаточно ли это улучшает работу разработчика, чтобы оправдать затраты? К сожалению, ответ часто бывает отрицательным.
В данном случае, вероятно, последнее. В Github есть проблема по этому поводу, но я бы не ожидал увидеть много работы над этим, если только много людей не начнут требовать этого.
Теперь для любого конкретного типа компилятор, как правило, сможет просмотреть и оценить задействованные конкретные типы и проверить эквивалентности:
interface Concrete {
a: string,
b: number,
c: boolean
}
// okay now
type OmitNothingConcrete =
CompilerKnowsTheseAreTheSame<Concrete, Omit<Concrete, never>>;
// nope, still too generic
type PickAndOmitConcrete<K extends keyof Concrete> =
CompilerKnowsTheseAreTheSame<Concrete, Pick<Concrete, K> amp; Omit<Concrete, K>>;
// okay now
type PickAndOmitConcreteKeys =
CompilerKnowsTheseAreTheSame<Concrete, Pick<Concrete, "a"|"b"> amp; Omit<Concrete, "a"|"b">>;
Но в вашем случае вы пытаетесь добиться того, чтобы это произошло с помощью generic T
, что не произойдет автоматически.
Когда вы знаете о задействованных типах больше, чем компилятор, есть вероятность, что вам может потребоваться разумное использование утверждения типа, которые являются частью языка как раз для такого случая:
function g<T extends A>(arg: Omit<T, 'x'>): T {
return { ...arg, x: "" } as T; // no error now
}
Ну вот, теперь он компилируется, и вы закончили, верно?
Что ж, давайте не будем слишком торопиться. Одна из ловушек при использовании утверждений типа заключается в том, что вы говорите компилятору не беспокоиться о проверке чего-либо, когда вы точно знаете, что то, что вы делаете, безопасно. Но знаете ли вы это? Это зависит от того, ожидаете ли вы увидеть некоторые крайние случаи. Вот то, что меня больше всего беспокоит в вашем примере кода.
Допустим, у меня есть разделенный тип U
объединения, который предназначен для либо хранения a
свойства , либо a b
property, в зависимости от значения строкового литерала x
свойства:
// discriminated union U
type U = { x: "a", a: number } | { x: "b", b: string };
declare const u: U;
// check discriminant
if (u.x === "a") {
console.log(u.a); // okay
} else {
console.log(u.b); // okay
}
Никаких проблем, верно? Но подождите, U
расширяется A
, потому что любое значение типа U
также должно быть значением типа A
. Это означает, что я могу вызывать g
следующим образом:
// notice that the following compiles with no error
const oops = g<U>({ a: 1 });
// oops is supposed to be a U, but it's not!
oops.x; // is "a" | "b" at compile time but "" at runtime!
Значение {a: 1}
может быть присвоено Omit<U, 'x'>
, и поэтому компилятор считает, что он создал значение oops
этого типа U
. Но это не так, не так ли? Вы знаете, что oops.x
этого не будет ни "a"
, ни "b"
во время выполнения, а скорее ""
. Мы солгали компилятору, и теперь у нас будут проблемы позже, когда мы начнем использовать oops
.
Теперь, возможно, с вами не случится такого крайнего случая, и если это так, вам не стоит сильно беспокоиться об этом … в конце концов, ввод текста должен упростить, а не усложнить поддержку кода.
Наконец, я хочу упомянуть, что g()
функция при вводе никогда не сможет определить тип для T
, который является более узким, чем A
. При вызове g({a: 1})
, T
будет выведено как A
. Если T
всегда выводится как A
, то у вас может даже не быть универсальной функции.
Возможно, по той же причине, по которой компилятор не может заглянуть в Omit<T, 'x'>
достаточно, чтобы понять, как он может объединяться с Pick<T, 'x'>
для формирования T
, он не может заглянуть в значение типа Omit<T, 'x'>
и выяснить, что T
должно быть. Итак, что можно сделать?
Компилятору намного проще определить тип фактического значения, которое вы ему передаете, поэтому давайте попробуем это:
function g<T>(arg: T) {
return { ...arg, x: "" };
}
Теперь g()
примет значение типа T
и вернет значение типа T amp; {a: string}
. В конечном итоге это всегда будет присваиваться A
, поэтому вы должны быть в порядке, чтобы использовать его:
const okay = g({a: 1, b: "two"}); // {a: number, b: string, x: string}
const works: A = okay; // fine
Если каким-то образом вы хотите запретить параметрам g()
иметь x
свойство, этого не произошло:
const stillWorks = g({x: 1});
но мы можем сделать это с ограничением на T
:
function g<T extends { [K in keyof T]: K extends 'x' ? never : T[K] }>(arg: T) {
return { ...arg, x: "" };
}
const breaksNow = g({x: 1}); // error, string is not assignable to never.
Это довольно типобезопасно, не требует утверждений типа и удобнее для вывода типа. Так что, вероятно, на этом я это оставлю.
Хорошо, надеюсь, эта новелла помогла вам. Удачи!
Комментарии:
1. Большое спасибо за ответ @jcalz, это дало мне гораздо лучшее понимание того, что происходит за кулисами! Я надеюсь, что этот сценарий станет возможным и очень простым, если и когда будут выпущены отрицаемые типы ( github.com/Microsoft/TypeScript/pull/29317 ).