2

I have declared an object like so:

const example = {
  morning: (name) => `Good morning ${name}!`,
  evening: (name) => `Good evening ${name}!`
}

In its raw form, the object has strongly typed keys (I can access them using dot notation), but the values could be anything. That's why I decided to introduce a type signature:

const example: {
  [K: string]: (name: string) => string;
} = {
  morning: (name) => `Good morning ${name}!`,
  evening: (name) => `Good evening ${name}!`
}

Now my object values are strongly typed, but the keys aren't, and I could try to access any key using dot notation without TypeScript complaining (for example, I could write console.log(example.doesntexist), and I wouldn't get editor autocompletion for the object keys either). So I tried this:

const example: {
  [K in keyof typeof example]: (name: string) => string;
} = {
  morning: (name) => `Good morning ${name}!`,
  evening: (name) => `Good evening ${name}!`
}

Now I'm getting a circular constraint error (Type parameter 'K' has a circular constraint and 'example' is referenced directly or indirectly in its own type annotation). I'm aware I could do something like this

const example: {
  [K in 'morning' | 'evening']: (name: string) => string;
} = {
  morning: (name) => `Good morning ${name}!`,
  evening: (name) => `Good evening ${name}!`
}

but that's redundant (and I hate redundancy/code duplication). Is there a way to do what I want to?

3x071c
  • 955
  • 10
  • 40
  • "*but the values could be anything*" - no, they're string-returning functions. What's wrong with that? – Bergi May 30 '21 at 14:42
  • @Bergi I want to require all values to be string-returning functions, to avoid errors down the road – 3x071c May 30 '21 at 14:46
  • why you declared type for `example` object ?, ts will infer that for you! – Nur May 30 '21 at 17:02
  • @Nur As I already said, I'm setting a type to require that all object values must be string-returning functions, so I can expect the same behavior from every item in my object – 3x071c May 30 '21 at 17:30
  • 1
    Plus you want auto completions right? in my knowledge you also need specified `keys` , like `let example: Record<'morning' | 'evening', () => string>;` – Nur May 30 '21 at 17:44
  • @SearchingSolutions If something in your code *expects* the same behavior of all functions in the object (e.g. when accepting it as a parameter), that's the place where you should declare that type. Or you can [write an assertion that checks your `example` conforms to a specific type](https://stackoverflow.com/q/55046211/1048572). – Bergi May 30 '21 at 20:13

1 Answers1

2

There's no way to do this without at least some extra code.

In cases like this I usually write a constrained generic identity function helper function. This behaves very much like the circular annotation you were trying to write:

const asExample = <K extends PropertyKey>(
  x: { [P in K]: (name: string) => string }
) => x;

This function will only accept an input whose type has keys K and values (name: string) => string) for some key-like K the compiler infers, and it returns the input. Because the function is generic in K, the compiler will keep track of K instead of widening to something like string.

Let's test it:

const example = asExample({
    morning: name => `Good morning ${name}!`,
    evening: name => `Good evening ${name}!`
})

example.morning("Alice") // okay
example.day // error! 
// ---> ~~~
// Property 'day' does not exist on type
// '{ morning: (name: string) => string; evening: (name: string) => string; }'

You can see that example is known to have properties at the morning and evening keys, but not at the day key.

And if you try to call asExample() on an bad input, you'll get the errors you want:

const badExample = asExample({
    afternoon: name => `Good afternoon ${name}!`.length, // error!
    // --------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // number is not assignable to string
    night: "Good night everyone!" // error!
//  ~~~~~ <-- string is not assignable to (name: string) => string
})

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360