0

I have a constant object like so:


const shortcuts = {
    save: {
        label: 'ctrl+s',
        action: (documentId: string) => saveDocument(documentId) 
    },
    open: {
        label: 'ctrl+o',
        action: (name: string, type: string) => loadDocument(documentName, extension),
        thisShouldError: 'thisShouldError' // not allowed property
    },
    // etc...
} as const
   

I would like to ensure all the shortcuts follow a desired shape shown below with no excess properties added.

interface DesiredShape {
    label: string,
    action: (...args: any[]) => void
}

In particular, I'd like the above definition of shortcuts to error because open.thisShouldError is not allowed. At the same time, I don't want to lose any type information, i.e. when I do shortcuts.save.action, I want to get the actual type signature of the function as (documentId: string) => saveDocument(documentId), not just the generic (...args: any[]) => void.

Is it possible to achieve this?

What I have tried so far:

I can use object literals limitations to do this, but then I lose the type information.

I also tried making sure the difference of the keys is never as below, but it seems TS only takes the keys which are present everywhere into ActualKeys.

type ActualKeys = keyof typeof shortcuts['save' | 'open']
type PermittedKeys = keyof DesiredShape
// does not work because ActualKeys does not include `thisShouldError`
assertNever(null as unknown as Exclude<ActualKeys,PermittedKeys>)

Playground Link

1 Answers1

1

I'm not sure why you want to do it, but one way of doing is:

type allKeys<T> = T extends {} ? keyof T: never;
type ActualKeys = allKeys<typeof shortcuts[keyof typeof shortcuts]>
type PermittedKeys = keyof DesiredShape
assertNever(null as Exclude<ActualKeys,PermittedKeys>)
Nadia Chibrikova
  • 4,916
  • 1
  • 15
  • 17
  • That's perfect, thank you! By the way, you mention this is "one way of doing this", would you mind sharing other ideas you might have had? I was quite surprised it is not at all straightforward to prevent users from adding excess properties to interfaces. It seems like quite a basic thing to do. – SquattingSlavInTracksuit May 01 '21 at 12:41
  • I think people are mainly concerned with the correct shape of properties they need, rather than absence of those they don't need (I can only imagine this being an issue if you have to deal with a particularly picky API). Instead of assetNever I'd do something like `let test : PermittedKeys = {} as ActualKeys; ` but it doesn't make much difference, and I don't think there is a way to make the actual `shortcuts` error as you need its type to be inferred. – Nadia Chibrikova May 01 '21 at 13:09