Home > Back-end >  Enforce interface for array of objects and also create type from a mapped value
Enforce interface for array of objects and also create type from a mapped value

Time:03-02

I'm using typescript to make sure queues fulfill the IQueue interface:

export interface IQueue {
  id: string;
  handler: () => void;
}

const queues:IQueue[] = [
  { id: 'a', handler: () => { } },
  { id: 'b' }, // handler is missing, should be an error
];

I also want a QueueId type which is a union of all the ids:

const queues = [
  { id: 'a', handler: () => { } },
  { id: 'b' },
] as const;


export declare type QueueId = (typeof queues[number])['id'];

export const start = (queueId:QueueId) => {
  ...
};

start('z'); // should be a typescript error

But I can't get them to work together. The QueueId type requires an as const type. Several posts recommend doing a noop cast but I get the readonly cannot be assigned to the mutable type... error. So I tried making it writeable but it gives an "insufficient overlap" error:

type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
(queues as DeepWriteable<typeof queues>) as IQueue[];

Is it possible to do both?

Here's a full example:

https://www.typescriptlang.org/play?#code/LAKAlgdgLgpgTgMwIYGMYAICSBFArjfdAb1HXTABMAudAZyjkgHMBuU9ACyQgoBt4aACgCU6ALwA dADcA9pTYgAvqFApZEeugCO fLXHoA2uxIgyZSjQDkAD2sAadmS49 cIaMnF0Sp d9-MjMLcmp0awBPR2d0AHo49CgOMANaDllcXgp0ACMMbnR4OFk4dEFXPnhyAzgYXTA6imF2P1AAXXQkA3VNKEVQChgUXiQ6pMiABww8AhhMHLFyqCmYWQQdPRhaIwhcAFt8uHbhI2tKa3aBkASk7agkjhmthYnptQ0tWHpZ-AWqX7zRYRABe1mutwAyhkAO5FOAlMpgDbcSKbOboCiybYQawPBBZXhosD7Sb8fYwaBYQGgFbTdAAERgMEmAHVGLAkLl AAeAAqUiWRHQAFo6kgsRAicYAArkCDoADWMEi63QfPaNCZLPZYE53Jg-KMMvaUiUikEujmBm6jOZbI5MC5vLpaw2Vv0ElEtpwWyMV1UN0SADlZA9aLhJpNSrAcho3tsUIxJlBRhAmAA6UpMBx5XAPGC2aNwB7JDAe7agW6F4sPIbILIPCs2gy ub xRAA

CodePudding user response:

First, if you want the compiler to infer string literal types for the id properties without inferring a readonly tuple type for queues, then you can move the const assertion from the queues initializer to just the id properties in question:

const queues = [
  {
    id: 'x' as const,
    handler: () => { },
  },
  {
    id: 'y' as const,
    handler: () => { },
  },
];

/* const queues: ({
     id: "x";
     handler: () => void;
   } | {
     id: "y";
     handler: () => void;
   })[] */

type QueueId = (typeof queues[number])['id'];
// type QueueId = "x" | "y"

At this point you want to check that queues's type is assignable to IQueue[] without actually actually annotating it as IQueue[], since that would make the compiler forget about "x" and "y" entirely.

TypeScript doesn't currently have a built-in type operator to do this; there is a feature request for one (tentatively) called satisfies at microsoft/TypeScript#47920 where you would maybe write something like

// this is not valid TS4.6-, don't try it:
const queues = ([
  {
    id: 'x' as const,
    handler: () => { },
  },
  {
    id: 'y' as const,
    handler: () => { },
  },
]) satisfies IQueue[];

And then the compiler would complain if you left out a handler or something. But there is no satisfies operator.

Luckily you can essentially write a helper function which (if you squint at it) behaves like a satisfies operator. Instead of writing x satisfies T, you'd write satisfies<T>()(x). Here's how you write it:

const satisfies = <T,>() => <U extends T>(u: U) => u;

That extra () in there is because satisfies is a curried function in order to allow you to specify T manually while having the compiler infer U. See Typescript: infer type of generic after optional first generic for more information.

Anyway, when we use it, we can see that it will complain if you mess up:

const badQueues = satisfies<IQueue[]>()([
  {
    id: 'x' as const,
    handler: () => { },
  },
  { id: 'y' as const }, // error!
  // ~~~~~~~~~~~~~~~~~ <-- Property 'handler' is missing
]);

And when you don't mess up, it doesn't forget about 'x' and 'y':

const queues = satisfies<IQueue[]>()([
  {
    id: 'x' as const,
    handler: () => { },
  },
  {
    id: 'y' as const,
    handler: () => { },
  },
]);

/* const queues: ({
     id: "x";
     handler: () => void;
   } | {
     id: "y";
     handler: () => void;
   })[]
*/

type QueueId = (typeof queues[number])['id'];
// type QueueId = "x" | "y"

Looks good!

Playground link to code

  • Related