Sri
Sri

Reputation: 271

Typescript function parameter must be a `const assertion` of a number

How do I create a function that only accepts a const assertion of a number. Eg:

function func(arg: number as const){} // somehow do this

func(1 as const); // valid
func(1); // error

Is this possible?

The reason I want to do this is because I want to return a tuple of length = arg

I do have a type, that create a tuple of given length:

type TupleFromCount<
    T,
    N extends number,
    S extends T[] = [],
> = S['length'] extends N ? S : TupleFromCount<T, N, [T, ...S]>;

type temp = TupleFromCount<string, 5>;
//    ^  type temp = [string, string, string, string, string]

So I want to use it like this:

function getRecords<T extends number = 10, K>(
    page: number,
    itemsPerPage: T,
): TupleFromCount<K, T> {
    const arr: TupleFromCount<string, T> = new Array<K>(itemsPerPage);
    return arr;
}

Upvotes: 5

Views: 2077

Answers (1)

jcalz
jcalz

Reputation: 328618

There isn't a way to require that the caller literally writes as const as part of the parameter. You probably don't even really want that anyway; presumably you just want the function to require that arg be of a numeric literal type instead of the number type.


The first step toward achieving this is to give the compiler a hint that you want the type of arg to be inferred as a numeric literal if possible... this would be more like requesting a const assertion interpretation of the input instead of requiring it. There's no similar syntax like as const in the call signature side, (see microsoft/TypeScript#30680 for a relevant feature request), but there are other ways to get this behavior. You could make func() a generic function and have the type parameter T be constrained to number. A type constraint like T extends number is a hint that the compiler should try to infer a numeric literal for T instead of number. (See microsoft/TypeScript#10676 which implements and describes this behavior).

So let's try it:

function func<T extends number>(arg: T) { }
func(1 as const); // function func<1>(arg: 1): void
func(1); // no error, but function func<1>(arg: 1): void

Here the compiler infers that T is 1 (and not number) in both the func(1 as const) call, as well as the plain func(1) call. This is probably even better than spewing errors when you call func(1), right?


That's great, but it still doesn't prevent number from being passed, like this:

let someNumber: number = 1;
func(someNumber); // no error,
// function func<number>(arg: number): void

To truly prevent that, we need to get creative with our generic constraints, and employ conditional types:

type SomeNumericLiteral 
  = 1 | 2 | 3; // just a dummy numeric literal union to make the error nice
function func<T extends (number extends T ? SomeNumericLiteral : number)>(
  arg: T) { }

What I've done there is to have the compiler look at arg and infer T. It then checks that it is assignable to number extends T ? SomeNumericLiteral : number. If T is inferred as a numeric literal, or really any type that isn't number or some supertype of it, then the conditional type collapses to number, and it behaves the same as before with just T extends number:

func(1 as const); // function func<1>(arg: 1): void
func(1); // function func<1>(arg: 1): void

On the other hand, if T is inferred as number or some supertype of it, then the conditional type collapses to SomeNumericLiteral, and T extends SomeNumericLiteral will fail the constraint, because number or some supertype does not extend SomeNumericLiteral by design:

let someNumber: number = 1;
func(someNumber); // error!
//   ~~~~~~~~~~
// Argument of type 'number' is not assignable to parameter of type 'SomeNumericLiteral'.

Note that I defined SomeNumericLiteral as 1 | 2 | 3, but we just needed to use some type that number doesn't extend to make a compiler error happen; I chose 1 | 2 | 3 since hopefully that will make the error message nicer in cases people pay close attention to it. The usual go-to thing to write is never instead of SomeNumericLiteral, but that makes a weird message.


So there you go... now func() only accepts numeric literals and rejects number.

Playground link to code

Upvotes: 3

Related Questions