Search code examples
typescripttyping

Error when passing an Object to a function expecting a Dictionnary?


I have the code below, where I want to pass a request object to an Http Get function

Why does get accept the object from helperObj and not the interface Req from helperInt

Link to Typescript Playground

interface Req {
    key: string;
    page?: number;
}

function helperInt(req: Req) {
    get('', req); // ERROR
    // Argument of type 'Req' is not assignable to parameter of type '{ [key: string]: string | number | boolean | (string | number | boolean)[]; }'.
    // Index signature for type 'string' is missing in type 'Req'.(2345)
}

function helperObj(req: {
    key: string;
    page?: number;
}) {
    get('', req);
}

function get(url: string, params?: { [key: string]: string | number | boolean | (string | number | boolean)[] }) {
    console.log(url, params);
}

const myReq: Req = {
    key: "foo",
    page: 0,
};

helperInt(myReq);
helperInt({
    key: "foo",
    page: 0,
});

helperObj(myReq);
helperObj({
    key: "foo",
    page: 0,
});

Notes:

  • Both helperInt and helperObj accept both a Req typed object or a literal object
  • The get function's params type is supposed to match Angular's HttpParams
  • Req interface is used to initialise an object, alter it if needed, before passing it to the get function

Solution

  • This isn't really an Angular question, it's a Typescript question.

    It boils down to:

    interface Req {
        key: string;
        page?: number;
    }
    
    // Let's make a type alias for the type `get` expects.
    type MyMap = { [key: string]: string | number | boolean | (string | number | boolean)[]; }
    
    const myReq: Req = {key: "foo",page: 0}; // this works
    const x: MyMap = { key: "foo", page: 0 }; // this works too
    
    const y: MyMap = myReq; // Type 'Req' is not assignable to type 'MyMap'. Index signature for type 'string' is missing in type 'Req'
    
    

    So what's the problem?

    Well, if Typescript allowed this, you could do:

    const x: Req = { key: 'foo', page: 0 };
    const y: MyMap = x;
    y.key = 0;
    y.page = true;
    console.log(x.key.toUppercase());
    

    x has type Req, so static typing says x.key.toUppercase() is safe. But we've managed to assign a non-string to x.key, since x and y are references to the same object.

    Therefore, Typescript disallows this.

    One way to work around it is to spread the object into a new object.

    const y: MyMap = { ...myReq };
    

    Typescript can see that the object you're creating is consistent with MyMap, but since it's a new object subsequent changes won't break the type contract.