5

I have a method, that should accepts any object, as long as all its fields are strings or numbers

I made this, which works great with duck typing

static interpolateParams(
    route: string, 
    params: {[key: string] : string | number}) : string {

    const parts = route
        .split("/")
        .map(part => {
            const match = part.match(/:([a-zA-Z09]*)\??/);
            if (match) {
                if (!params[match[1]]) {
                    console.error("route argument was not provided", route, match[1]);
                    return part;
                }

                return params[match[1]];
            }
            else {
                return part;
            }
        })

    return "/" + parts.slice(1).join("/");
}

and call

interpolateParams("/api/:method/:value", {method: "abc", value: 10});

Now I want to make interpolateParams to accept any interface for route params.

interpolateParams<IABCParams>("/api/:method/:value", {method: "abc", value: 10});

Problem is that it still should match constraints for all fields being strings or numbers

Is there a way to specify generic constraint on all fields of given interface to be of certain type?

I tried that

static interpolateParams<T extends {[key: string] : string | number}>(
    route: string, 
    params: T) : string {

and obviously got this

Type 'IABCParams' does not satisfy the constraint '{ [key: string]: string | number; }'.

Index signature is missing in type 'IABCParams'.

Thanks

Andrew Shepherd
  • 44,254
  • 30
  • 139
  • 205
Evgeny
  • 181
  • 2
  • 8

3 Answers3

9

T's constraint can refer to T (with some restrictions), so you can use a mapped type to generate a constraint that has the same fields as T:

function interpolateParams<T extends {[P in keyof T] : string | number}>(
    route: string, 
    params: T) : string { /*...*/ }

Beware that trickery with circular type parameter constraints can sometimes cause problems, though this scenario will likely be OK.

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • Ok, that did not work quite well, but with additional layer it seems to tricked interpreter :) As you described, error in `if (!params[match[1]]) ` line - no indexer. So I added another version of interpolateRoute as you suggested, and inside call bare interpolate (see next for code) – Evgeny Oct 10 '18 at 23:55
2

Here is final version, thanks Matt for hint

static interpolateParams(
    route: string, 
    params: {[key: string] : string | number}) : string {

    const parts = route
        .split("/")
        .map(part => {
            const match = part.match(/:([a-zA-Z09]*)\??/);
            if (match) {
                if (!params[match[1]]) {
                    if (part.endsWith("?")) {
                        return null;
                    }

                    console.error("route argument was not provided", route, match[1]);
                    return part;
                }

                return params[match[1]];
            }
            else {
                return part;
            }
        }).filter(p => p && p != "");

    return "/" + parts.join("/");
}

static formatRoute<T extends {[P in keyof T] : string | number}>(
    route: string,
    params: T
) : string {
    return interpolateParams(route, params);
}
Evgeny
  • 181
  • 2
  • 8
1

If you want interpolateParams("/api/:method/:value", {method: "abc", value: 10}); to typecheck you can't. This is because you cannot infer anything about "/api/:method/:value" to give you a method,value signature.

Workaround

Write functions that take method and value and use it to power both the config and the use.

E.g. this is the strategy I use with takeme.

// Define 
export const links = {
  profile: (id: string) => `/profile/${id}`
}

// Use in Configure 
links.profile(':profileId')

// Use in Navigate / a tags
links.profile(user.id)
basarat
  • 261,912
  • 58
  • 460
  • 511