Система типов не предназначена для таких ограничений на дополнительные ключи объектов. Типы в TypeScript не являются точными: если объект имеет тип A
, и вы добавляете больше свойств, которые не упомянуты в определении A
, объект по-прежнему имеет тип A
. По сути, это необходимо для поддержки наследования классов, когда подклассы могут добавлять свойства к суперклассам.
Единственный раз, когда компилятор обрабатывает типы как точные, это когда вы используете «свежий» литерал объекта (то есть тот, который еще ничему не был присвоен) и передаете его чему-то, что ожидает тип объекта. Это называется проверкой дополнительных свойств и обходного пути для отсутствия точных типов в языке. Вы хотите, чтобы избыточная проверка свойств происходила с «несвежими» объектами, такими как test
, но этого не произойдет.
TypeScript не имеет конкретного представления для точных типов; вы не можете взять тип T
и произвести из него Exact<T>
. Но вы можете использовать общее ограничение, чтобы получить этот эффект. Учитывая тип T
и объект типа U
, который вы хотите согласовать с непредставимым типом Exact<T>
, вы можете сделать это:
type Exactly<T, U extends T> = {[K in keyof U]: K extends keyof T ? T[K] : never};
type IsExactly<T, U extends T> = U extends Exactly<T, U> ? true : false;
const testGood = {
g: 1
}
type TestGood = IsExactly<Class2, typeof testGood>; // true
const testBad = {
g: 6,
a: '6',
};
type TestBad = IsExactly<Class2, typeof testBad>; // false
Таким образом, компилятор может сказать, что typeof testGood
— это «Exactly<Class2, typeof testGood>
», а typeof testBad
— не Exactly<Class2, typeof testBad>
. Мы можем использовать это для создания универсальной функции, которая будет делать то, что вы хотите. (В вашем случае вам нужно что-то вроде ExactlyPartial<T, U>
вместо Exactly<T, U>
, но это очень похоже... просто не ограничивайте U
расширением T
).
К сожалению, ваша функция уже является универсальной в T
, типе, который нужно уточнить. И вы вручную указываете T
, универсальная функция должна определить тип U
. TypeScript не допускает вывод параметров частичного типа. Вы должны либо вручную указать все параметры типа в функции, либо позволить компилятору вывести все параметры типа в функции. Итак, есть обходные пути:
Один из них — разделить вашу функцию на curried, в которой первая общая функция позволяет указать T
, а возвращаемая универсальная функция выводит U
. Это выглядит так:
class Testing {
static testIt<T>(): <U extends { [K in keyof U]:
K extends keyof T ? T[K] : never
}> (val: U) => void {
return () => { }
}
}
Testing.testIt<Class2>()(testBad); // error, prop "a" incompatible
Testing.testIt<Class2>()(testGood); // okay
Это работает так, как вы ожидаете, но влияет на время выполнения, поскольку вам приходится вызывать каррированную функцию без какой-либо причины во время выполнения.
Другой обходной путь — передать функции значение, из которого можно вывести T
. Поскольку вам не нужно такое значение, это, по сути, фиктивный параметр. Опять же, это влияет на время выполнения, поскольку вам нужно передать значение, которое не используется. (Вы упомянули, что на самом деле вы можете использовать такое значение во время выполнения, и в этом случае это уже не обходной путь, а предлагаемое решение, поскольку вам все равно нужно что-то передать, а ручная спецификация T
в вашем примере кода была отвлекающий маневр.) Это выглядит так:
class Testing {
static testIt<T, U extends { [K in keyof U]:
K extends keyof T ? T[K] : never
}>(ctor: new (...args: any) => T, val: U) {
// not using ctor in here, so this is a dummy value
}
}
Testing.testIt(Class2, testBad); // error, prop "a" incompatible
Testing.testIt(Class2, testGood); // okay
Третий обходной путь, который я могу придумать, — это просто использовать систему типов для представления результата возврата каррированной функции без ее фактического вызова. Он вообще не влияет на время выполнения, что делает его более подходящим для предоставления типов существующему коду JS, но он немного неуклюж в использовании, поскольку вы должны утверждать, что Testing.testIt
действует правильно. Это выглядит так:
interface TestIt<T> {
<U extends { [K in keyof U]: K extends keyof T ? T[K] : never }>(val: U): void;
}
class Testing {
static testIt(val: object) {
// not using ctor in here, so this is a dummy value
}
}
(Testing.testIt as TestIt<Class2>)(testBad); // error, prop "a" incompatible
(Testing.testIt as TestIt<Class2>)(testGood); // okay
Хорошо, надеюсь, что один из них работает для вас. Удачи!
Link to code
person
jcalz
schedule
15.11.2019
T
, тогда вам нужно что-то вроде частичный вывод параметра типа, чтобы сделать то, что вы ищете for, который также не поддерживается. - person jcalz   schedule 14.11.2019T
, который вы вручную указываете какClass2
(иначе невозможно ничего запретить, так как любой объект будетPartial<T>
для некоторыхT
); и типU
, который выводится на основе типаval
. Общее ограничениеU extends { [K in keyof U]: K extends keyof T ? T[K] : never }
явно требует, чтобы все свойстваval
происходили из типаT
и не могли быть дополнительными. Если это соответствует вашим потребностям, я буду рад написать об этом. - person jcalz   schedule 15.11.2019U extends...
от типа, объявленного над функцией, а не как встроенный общий параметр функции возврата, как вы показали. - person CShark   schedule 15.11.2019U
, чтобы вы не могли переместитьU
. Во всяком случае, вы могли бы сделать это одной функцией, жестко запрограммировавT
какClass2
. - person jcalz   schedule 15.11.2019Class2
не используется, поэтому это фиктивное значение, единственная цель которого - помочь компилятору назначать типы. Я обычно называю это фиктивным, и это еще один обходной путь для вывода параметра частичного типа (см. этот ответ). Оба они имеют эффект времени выполнения. Если вы собираетесь использовать переданный конструкторClass2
в реализации функции, то, конечно, вы должны использовать его вместо каррирования, потому что значение больше не является фиктивным. Я напишу это как ответ в ближайшее время. - person jcalz   schedule 15.11.2019