Мне нужен способ ограничить параметр универсальной функции объединением буквальных значений, то есть
'foo'
,'foo' | 'bar'
,42
и т. Д. Должны быть разрешеныstring
илиnumber
не должныstring & SomethingElse
илиnumber & AnotherThing
также не должны('foo' | 'bar') & SomethingElse
,42 & AnotherThing
следует разрешить (менее важно)
Рассмотрим такую функцию
declare function test<K extends string>(t: { [P in K]: string; }): void;
Идея состоит в том, чтобы каждое возможное значение K
имело соответствующее свойство в t
, чтобы мы могли искать значения в t
на основе некоторых K
-типизированных значений.
Некоторые примеры использования, показывающие, что все идет хорошо:
declare const foobar: { foo: 'bar'; };
test(foobar); // compiles, correct
test<'foo'>(foobar); // compiles, correct
test<'bar'>(foobar); // compiler error, correct
// ^^^^^^
// Argument of type '{ foo: "bar"; }' is not assignable to parameter of type '{ bar: string; }'.
// Property 'bar' is missing in type '{ foo: "bar"; }' but required in type '{ bar: string; }'.
Однако есть проблема:
test<string>(foobar); // compiles, INCORRECT
В случае test<string>
{ [P in K]: string; }
становится { [P: string]: string; }
, что не гарантирует, что на самом деле существует ключ для каждого значения (и действительно, это было бы невозможно, если бы не были некоторые махинации с прокси-геттером). Поэтому я не хочу разрешать { [P: string]: string; }
аргументов в моей функции test
, что эквивалентно тому, что я не хочу, чтобы моя функция test
допускала string
в качестве значения для K
.
Теперь можно обойти это… своего рода. Учитывать
declare function test<K extends string>(t: string extends K ? never : { [P in K]: string; }): void;
Тест string extends K
сообщает нам, был ли string
передан в K
, и в таком случае мы делаем аргумент never
, предотвращая передачу каких-либо реальных значений функции (здесь лучше было бы вызвать ошибку, но Typescript не поддерживает тот). И так
test<string>(foobar); // compiler error, correct
// ^^^^^^
// Argument of type '{ foo: "bar"; }' is not assignable to parameter of type 'never'.
Однако это не решает проблему, когда мы используем строковые микшеры, как это делает наш проект. Учитывать
type StringExtended = string & { extension(): void; };
test<StringExtended>(foobar); // compiles, INCORRECT
Поскольку string extends StringExtended
ложно, наш never
хак не применяется, и мы возвращаемся к { [P in K]: string; }
, который, как ни странно, теперь обозначается как {}
, а не как { [P: string]: string; }
. В любом случае, он все равно компилируется. Я не хочу этого, потому что в нашем проекте используется много таких расширений (в частности, с использованием фирменные примитивы), и это слишком правдоподобно для некоторого K extends string
ограничения, которое было предназначено для строковых литералов, чтобы вместо этого принимать такие расширения строк, нарушая тип - безопасность функции.
Для более «реалистичного» примера рассмотрим
interface Keyed<K extends string, D> { key: K; data: D; }
function toNameOf<K extends string>(
{ key }: Keyed<K, any>,
names: string extends K ? never : { [P in K]: string; },
): string {
return names[key];
}
declare const k1: Keyed<'foo' | 'bar', number>;
toNameOf(k1, { foo: 'Foo', bar: 'Bar' });
toNameOf(k1, {});
// ^^
toNameOf(k1, { foo: 'Foo' });
// ^^^^^^^^^^^^^^
toNameOf(k1, { bar: 'Bar' });
// ^^^^^^^^^^^^^^
declare const k2: Keyed<string, number>;
toNameOf(k2, { foo: 'Foo', bar: 'Bar' });
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
toNameOf(k2, {});
// ^^
toNameOf(k2, { foo: 'Foo' });
// ^^^^^^^^^^^^^^
toNameOf(k2, { bar: 'Bar' });
// ^^^^^^^^^^^^^^
declare const k3: Keyed<StringExtended, number>;
toNameOf(k3, { foo: 'Foo', bar: 'Bar' });
toNameOf(k3, {});
toNameOf(k3, { foo: 'Foo' });
toNameOf(k3, { bar: 'Bar' });
k1
случаи правильно терпят неудачу, если мы не получаем names
аргумент, который фактически охватывает все возможности. А в k2
мы просто все отвергаем; k2
не поддерживает тип, который toNamesOf
поддерживает. Но в k3
случаях все компилируются без ошибок или предупреждений, несмотря на все мои усилия. И структуры, аналогичные Keyed
, которые заполнены (правильно, в других случаях использования) StringExtended
, а не каким-либо конкретным литералом, находятся повсюду в нашем коде.