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

46 views Asked by At

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:

  • Define a type that I can use to typecheck the structure of operations and
  • to have a Callbacks type that will typecheck my callback functions based on the structure of operations?
1

There are 1 answers

0
jcalz On BEST ANSWER

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