Есть ли в TypeScript способ ограничить дополнительные/лишние свойства для типа Partial‹T›, когда тип является параметром функции?

Есть ли стандартный способ заставить Сценарий 1 иметь ошибку компиляции из-за того, что не указаны известные свойства, как в Сценарии 2? Или есть обходной путь?

class Class2 {
  g: number;
}

class Testing {
  static testIt3<T>(val: Partial<T>): void {
  }
}

const test = {
  g: 6,
  a: '6',
};

// Scenario 1
Testing.testIt3<Class2>(test);
// TS does not show any errors for this scenario

// Scenario 2
Testing.testIt3<Class2>({
  g: 6,
  a: '6',
});
// but it does for this scenario:
// Object literal may only specify known properties...

Текущий код


person CShark    schedule 14.11.2019    source источник
comment
Я не думаю, что это можно обойти; TypeScript не имеет точных типов и лишние проверки свойств применяются только к литералам объектов, как в сценарии 2. Если вам нужно вручную укажите T, тогда вам нужно что-то вроде частичный вывод параметра типа, чтобы сделать то, что вы ищете for, который также не поддерживается.   -  person jcalz    schedule 14.11.2019
comment
Если вас устраивает каррирование, вы можете сделать что-то вроде this, I guess. Дайте мне знать, если вы хотите, чтобы я написал это как ответ.   -  person jcalz    schedule 14.11.2019
comment
@jcalz да, я только что нашел эту проблему с github, которая объясняет это   -  person CShark    schedule 14.11.2019
comment
@jcalz Значит, этот тип должен быть определен в функции, которую вы объявили в нашем примере?   -  person CShark    schedule 15.11.2019
comment
Я не уверен, что понимаю вопрос. Мой пример зависит от наличия двух универсальных типов: тип T, который вы вручную указываете как 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.2019
comment
@jcalz Извините, я не думаю, что очень хорошо сформулировал свой вопрос. Я просто пытался выяснить, есть ли способ написать это аналогичным образом, но без необходимости выполнять несколько вызовов функций. Поэтому я пытался спросить, могу ли я абстрагировать этот U extends... от типа, объявленного над функцией, а не как встроенный общий параметр функции возврата, как вы показали.   -  person CShark    schedule 15.11.2019
comment
Нет, он должен быть общим в U, чтобы вы не могли переместить U. Во всяком случае, вы могли бы сделать это одной функцией, жестко запрограммировав T как Class2.   -  person jcalz    schedule 15.11.2019
comment
@jcalz Итак, в моем реальном проекте я пытаюсь вывести типы на основе переданного типа, поэтому я возился с этим, используя вашу ссылку в качестве отправной точки. Думаю, я нашел другой способ сделать это.   -  person CShark    schedule 15.11.2019
comment
See here   -  person CShark    schedule 15.11.2019
comment
@jcalz Похоже, что моя ссылка дает то же сообщение об ошибке, но есть ли причина, по которой я должен предпочесть стратегию в вашем примере той, на которую я только что ссылался в своем предыдущем комментарии?   -  person CShark    schedule 15.11.2019
comment
В вашем примере переданное значение Class2 не используется, поэтому это фиктивное значение, единственная цель которого - помочь компилятору назначать типы. Я обычно называю это фиктивным, и это еще один обходной путь для вывода параметра частичного типа (см. этот ответ). Оба они имеют эффект времени выполнения. Если вы собираетесь использовать переданный конструктор Class2 в реализации функции, то, конечно, вы должны использовать его вместо каррирования, потому что значение больше не является фиктивным. Я напишу это как ответ в ближайшее время.   -  person jcalz    schedule 15.11.2019
comment
@jcalz да, в моем реальном проекте будет использоваться это переданное значение. Но я понимаю вашу точку зрения, что если это не так, то, вероятно, лучше использовать каррирование, чтобы избежать объявления нескольких параметров типа при вызове функции.   -  person CShark    schedule 15.11.2019


Ответы (1)


Система типов не предназначена для таких ограничений на дополнительные ключи объектов. Типы в 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
comment
Да, я просто объявлял тип вместо того, чтобы передавать его в качестве первого аргумента в моем первоначальном примере, потому что я думал, что это будет более минимальный пример, показывающий проблему, с которой я столкнулся. Я не подумал, что это может повлиять на то, как лучше всего спроектировать функцию. - person CShark; 15.11.2019