typescripttype-inferencemapped-types

Is there a way to infer the keys of a mapped type when using Typescript?


I have a data structure that represents my business operations:

const operations = {
    list: {
        request: "a",
        response: "b",
    },
    get: {
        request: "a",
        response: "b",
    },
} as const;

I want to create a function that will accept callbacks based on the operations above:

type Ops = typeof operations;

type Callbacks = {
    [Property in keyof Ops]: (
        param: Ops[Property]["request"]
    ) => Ops[Property]["response"];
};

Now if I want to define my callbacks the compiler will complain if I miss any of them:

const callbacks: Callbacks = {

};
// ^^^--- Type '{}' is missing the following properties from type 'Callbacks': list, get

Now my problem is that I want to create another type so that I can type check the operations object's structure:

interface OperationDescriptor<A, B> {
    request: A;
    response: B;
}

type Operations = {
    [key: string]: OperationDescriptor<any, any>;
};

const operations: Operations = {
    list: {
        request: "a",
        response: "b",
    },
    get: {
        request: "a",
        response: "b",
    },
} as const; // okay

const badOperations: Operations = {
    list: {
        request: "a",
        response: "b",
    },
    get: { // error, missing response prop
        request: "a",        
    },
} as const; 

but when I do this the compiler will no longer complain because Operations doesn't know about my keys in operations. Is there a way to have my cake and eat it too, eg:


Solution

  • You can use the satisfies operator to check that a value is assignable to a type without widening it to that type. It's made exactly for the situation where a type annotation would forget information you care about:

    const operations = {
        list: {
            request: "a",
            response: "b",
        },
        get: {
            request: "a",
            response: "b",
        },
    } as const satisfies Operations;
    

    If the above compiles then you know that you're okay. Otherwise you get the errors you expect:

    const badOperations = {
        list: {
            request: "a",
            response: "b",
        },
        get: { // error!
        //~ <-- Property 'response' is missing
            request: "a",
        },
    } as const satisfies Operations;
    

    And the type typeof operations is still exactly as detailed as you need it to be:

    type Ops = typeof operations;
    /* type Ops = {
        readonly list: {
            readonly request: "a";
            readonly response: "b";
        };
        readonly get: {
            readonly request: "a";
            readonly response: "b";
        };
    } */
    

    Playground link to code