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:
Playground
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With