Reputation: 446
I want to type a function that checks that it's argument's type extends a type variable, and then returns the argument.
Like this:
declare function extendsA<I, X extends I>(argument:X): X
But when I use it, typescript complains that the function takes two type arguments. Or this:
declare function extendsB<I, X extends I = I>(argument:X): X
And that returns the wrong type.
How do I make typescript take the type of argument
put it into the type variable X
, ensure that it extends I
and then return X
?
A use case would be:
const recordOfNumberArray = extendsA<{[K: string]: readonly number[]}>({
"hello":[10,200]
} as const); // good
recordOfNumberArray. // with intellisense of all keys
const recordOfNumberArray = extendsA<{[K: string]: readonly number[]}>({
"hello":[10,200,"hi"]
} as const); // should error
Upvotes: 3
Views: 1590
Reputation: 329598
Ah, you're trying to implement the so-called "satisfies
" operator, as requested in microsoft/TypeScript#7481. The idea there is that you could write val satisfies Type
, and there'd be a compiler error if val
is not of type Type
, but it does not widen val
to type Type
. Unfortunately such an operator does not yet exist in TypeScript, so you are resorting to a workaround. I'm going to rename extendsA
to satisfies
from here on.
So you can't easily implement satisfies
as a single generic function of two type parameters, since TypeScript also lacks partial type parameter inference as requested in microsoft/TypeScript#26242. Either the compiler will infer all type parameters from the call, or you have to manually specify all the type parameters. As you discovered, a generic type parameter default cannot be used to give you partial inference, since all you'll get is the default if you leave out the parameter.
Instead, the normal way around this is to refactor to a curried function, where the function takes only the generic type parameter to be manually specified, and then it returns another function which takes the type parameter to be inferred. Like this:
// declare function satisfies<I>(): <X extends I>(argument: X) => X;
function satisfies<I>() {
return <X extends I>(argument: X) => argument
}
And therefore you have to call it like this, with an extra function call step:
satisfies<{ [K: string]: readonly number[] }>()({
"hello": [10, 200]
} as const); // good
It's not wonderful, although the pain can be lessened if you find yourself checking for the same I
type more than once, since you can reuse the partially applied function:
const satisfiesRecordOfNumberArray =
satisfies<{ [K: string]: readonly number[] }>();
const recordOfNumberArray = satisfiesRecordOfNumberArray({
"hello": [10, 200]
} as const); // good
recordOfNumberArray.hello // okay
const recordOfNumberArray2 = satisfiesRecordOfNumberArray({
"hello": [10, 200, "hi"]
} as const); // error!
// Type 'readonly [10, 200, "hi"]' is not assignable to type 'readonly number[]'
The only other approach I can think of is to keep the two type parameters in one function, I
and X
, but add a dummy function parameter dummyI
of type I
. This is probably even worse than currying, since it forces you to provide a dummy value of type I
, or pretend to:
// declare function satisfies<I, X extends I>(dummyI: I, argument: X): X;
function satisfies<I, X extends I>(dummyI: I, argument: X) { return argument }
const recordOfNumberArray = satisfies(null! as { [K: string]: readonly number[] }, {
"hello": [10, 200]
} as const); // good
const recordOfNumberArray2 = satisfies(null! as { [K: string]: readonly number[] }, {
"hello": [10, 200, "hi"]
} as const); // error!
// Type 'readonly [10, 200, "hi"]' is not assignable to type 'readonly number[]'
That works, but now instead of satisfies<I>()(val)
you have satisfies(null! as I, val)
. As I said, probably worse, although it's subjective. Ideally you wouldn't need any function calls and there'd be a built-in satisfies
operator. Without a built-in operator, the jump from one function call to two function calls doesn't seem much worse than the jump from zero to one.
Upvotes: 4