Let's say I have a generic interface like the following:
interface Transform<ArgType> {
transformer: (input: string, arg: ArgType) => string;
arg: ArgType;
}
And then I want to apply an array of these Transform
to a string
. How do I define this array of Transform
such that it validates that <ArgType>
is equivalent in both Transform.transformer
and Transform.arg
? I'd like to write something like this:
function append(input: string, arg: string): string {
return input.concat(arg);
}
function repeat(input: string, arg: number): string {
return input.repeat(arg);
}
const transforms = [
{
transformer: append,
arg: " END"
},
{
transformer: repeat,
arg: 4
},
];
function applyTransforms(input: string, transforms: \*what type goes here?*\): string {
for (const transform of transforms) {
input = transform.transformer(input, transform.arg);
}
return input;
}
In this example, what type do I define const transforms
as in order for the type system to validate that each item in the array satisfies the generic Transform<ArgType>
interface?
(Using TS 3.0 in the following)
If TypeScript directly supported existential types, I'd tell you to use them. An existential type means something like "all I know is that the type exists, but I don't know or care what it is." Then your transforms
parameter have a type like Array< exists A. Transform<A> >
, meaning "an array of things that are Transform<A>
for some A
". There is a suggestion to allow these types in the language, but few languages support this so who knows.
You could "give up" and just use Array<Transform<any>>
, which will work but fail to catch inconsistent cases like this:
applyTransforms("hey", [{transformer: repeat, arg: "oops"}]); // no error
But as you said you're looking to enforce consistency, even in the absence of existential types. Luckily, there are workarounds, with varying levels of complexity. Here's one:
Let's declare a type function which takes a T
, and if it a Transform<A>
for some A
, it returns unknown
(the new top type which matches every value... so unknown & T
is equal to T
for all T
), otherwise it returns never
(the bottom type which matches no value... so never & T
is equal to never
for all T
):
type VerifyTransform<T> = unknown extends
(T extends { transformer: (input: string, arg: infer A) => string } ?
T extends { arg: A } ? never : unknown : unknown
) ? never : unknown
It uses conditional types to calculate that. The idea is that it looks at transformer
to figure out A
, and then makes sure that arg
is compatible with that A
.
Now we can type applyTransforms
as a generic function which only accepts a transforms
parameter which matches an array whose elements of type T
match VerifyTransform<T>
:
function applyTransforms<T extends Transform<any>>(
input: string,
transforms: Array<T> & VerifyTransform<T>
): string {
for (const transform of transforms) {
input = transform.transformer(input, transform.arg);
}
return input;
}
Here we see it working:
applyTransforms("hey", transforms); // okay
If you pass in something inconsistent, you get an error:
applyTransforms("hey", [{transformer: repeat, arg: "oops"}]); // error
The error isn't particularly illuminating: "[ts] Argument of type '{ transformer: (input: string, arg: number) => string; arg: string; }[]' is not assignable to parameter of type 'never'.
" but at least it's an error.
Or, you could realize that if all you're doing is passing arg
to transformer
, you can make your existential-like SomeTransform
type like this:
interface SomeTransform {
transformerWithArg: (input: string) => string;
}
and make a SomeTransform
from any Transform<A>
you want:
const makeSome = <A>(transform: Transform<A>): SomeTransform => ({
transformerWithArg: (input: string) => transform.transformer(input, transform.arg)
});
And then accept an array of SomeTransform
instead:
function applySomeTransforms(input: string, transforms: SomeTransform[]): string {
for (const someTransform of transforms) {
input = someTransform.transformerWithArg(input);
}
return input;
}
See if it works:
const someTransforms = [
makeSome({
transformer: append,
arg: " END"
}),
makeSome({
transformer: repeat,
arg: 4
}),
];
applySomeTransforms("h", someTransforms);
And if you try to do it inconsistently:
makeSome({transformer: repeat, arg: "oops"}); // error
you get an error which is more reasonable: "Types of parameters 'arg' and 'arg' are incompatible. Type 'string' is not assignable to type 'number'.
"
Okay, hope that helps. Good luck.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With