1

Having a generic function that takes a string-indexed object

function wantsStringIndexedObject<
    Obj extends { [k: string]: any }, 
    K extends keyof Obj
  >(obj: Obj, key: K) {

  const _ktype = typeof key
  // const ktype: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"

  const val = obj[key]

  wantsStringAndAny(key, val)
  // Argument of type 'K' is not assignable to parameter of type 'string'.
  // Type 'keyof Obj' is not assignable to type 'string'.
  //    Type 'string | number | symbol' is not assignable to type 'string'.
  //    Type 'number' is not assignable to type 'string'.
  // (parameter) key: K extends keyof Obj
} 

// no error here
wantsStringIndexedObject({
  1: 10,
  [Symbol()]:1
}, 1) 

function wantsStringAndAny(str: string, v:any) {} 

TS playground

I know I could use a type guard on key before calling wantsStringAndAny but I would expect:
1: key should be typed as string for sure inside wantsStringIndexedObject
2: shouldn't be allowed to call wantsStringIndexedObject with that kind of object

I suppose that the issue comes from generic definition Obj extends { [k: string]: any }
infact any object with any index type extends a { [k: string]: any } with 0 properties.

so, is there a way to define a generic ensuring a string-indexed-object constraint ?

aleclofabbro
  • 1,635
  • 1
  • 18
  • 35

1 Answers1

3

You can ensure the key is of a string type by using the Extract conditional type to filter the key of Obj:

function wantsStringAndAny(str: string, v:any) {

} 

function wantsStringIndexedObject<
    Obj extends object, 
    K extends Extract<keyof Obj, string>
  >(obj: Obj, key: K) {

  const _ktype = typeof key
  const val = obj[key]

  wantsStringAndAny(key, val)
} 

// err
wantsStringIndexedObject({
  1: 10,
  [Symbol()]:1
}, 1)

//ok
wantsStringIndexedObject({
  "1": "",
}, "2")

Generally a constraint is the minimal contract a type must implement, nothing stopping the actual type from having symbols amongst it's keys as well. Also string index will actually allow indexing with both strings and numbers, so numbers would be allowed under your supposition as well.

If you want to also forbid any number or symbol keys from Obj you could do the following:

function wantsStringIndexedObject<
    Obj extends object, 
    K extends Extract<keyof Obj, string>
  >(obj: Obj & Record<Extract<keyof Obj, number | symbol>, never>, key: K) {

  const _ktype = typeof key
  const val = obj[key]

  wantsStringAndAny(key, val)
} 

//ok
wantsStringIndexedObject({
  "1": "",
}, "1")
wantsStringIndexedObject({
  "1": "",
  2: 0 // errr
}, "1")

This takes any number or symbol keys and basically says they should be of type never (possible but unlikely) and you will get an error on any such keys.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • This still allows the caller to provide an object with non-string keys (such as the `[Symbol()]` in the question), even though it doesn't allow a non-string for the `key` parameter. Also, why didn't the OP's approach `extends { [key: string]: any }` work? – T.J. Crowder Apr 22 '19 at 17:22
  • @T.J.Crowder fine, fine I'll expand on the answer, I just wanted to quickly run away with my points :P – Titian Cernicova-Dragomir Apr 22 '19 at 17:23
  • LOL. While you're doing that, I'm wondering (genuinely) why the above would be better than just `wantsStringIndexedObject(obj: object, key: string)`. – T.J. Crowder Apr 22 '19 at 17:25
  • 2
    @T.J.Crowder well `wantsStringIndexObject(obj: object, key: string)` you can pass in `wantsStringIndexObject({ a: 0 }, "b")`. With the approach above the key passed in must be a key of the first parameter .. – Titian Cernicova-Dragomir Apr 22 '19 at 17:29
  • @T.J.Crowder added the extra info, hope it's clear. Gonna re-read each time I get an upvote to correct spelling mistakes :P – Titian Cernicova-Dragomir Apr 22 '19 at 17:34