1

In a previous question, I asked about assigning a value of a object and the key of an object. Now that I have implemented it, the first function works fine when using the keyof but the second does not let me switch on the key to narrow down the type.

Below is example code that has comments next to the relevant lines.

type JWT = { id: string, token: string, expire: Date };
const obj: JWT = { id: 'abc123', token: 'tk01', expire: new Date(2018, 2, 14) };


function print(key: keyof JWT) {
    switch (key) {
        case 'id':
        case 'token':
            console.log(obj[key].toUpperCase());
            break;
        case 'expire':
            console.log(obj[key].toISOString()); // Works!
            break;
    }
}

function onChange<K extends keyof JWT>(key: K, value: JWT[K]) {
    switch (key) {
        case 'id':
        case 'token':
            obj[key] = value + ' (assigned)';
            break;
        case 'expire':
            obj[key] = value.toISOString(); // Error!
            break;
    }
}

How can I implement the onChange function so that switch will narrow down the type similar to the print function above?

styfle
  • 22,361
  • 27
  • 86
  • 128
  • 2
    It's a [design limitation](https://github.com/Microsoft/TypeScript/issues/17859) that TypeScript only narrows the type of the switched `key` and not the related types of `K` or `value`. Short answer might be that you need to just assert `(value as Date).toISOString()` in that case. – jcalz Mar 16 '18 at 02:17
  • Uh, of course, `obj[key]` should be a `Date` and not a `string` in that case anyway. – jcalz Mar 16 '18 at 02:19

1 Answers1

4

A bit crazy but I think achieves what you desire ;) It builds upon my answer to your previous question about valueof. Also it changes onChange signature to accept an object of type {key: K extends string, value: JWT[K]}, instead of separate key and value parameters

type JWT = { id: string; token: string; expire: Date }
const obj: JWT = { id: 'abc123', token: 'tk01', expire: new Date(2018, 2, 14) }

function print(key: keyof JWT) {
  switch (key) {
    case 'id':
    case 'token':
      console.log(obj[key].toUpperCase())
      break
    case 'expire':
      console.log(obj[key].toISOString()) // Works!
      break
    default:
      return
  }
}

type ObjectToUnion<
  T extends object,
  U = { [K in keyof T]: { key: K; value: T[K] } }
> = U[keyof U]

function onChange(jwt: ObjectToUnion<JWT>) {
  switch (jwt.key) {
    case 'id': // Try to comment this line and see the error appear at 'exhaustiveCheck'
    case 'token':
      obj[jwt.key] = jwt.value + ' (assigned)'
      return
    case 'expire':
      obj[jwt.key] = jwt.value.toISOString() // Error!
      return
    default:
      // Optionally add exhaustive check to make sure you have covered all cases :)
      const exhaustiveCheck: never = jwt
      return
  }
}

You can read more about never type here.

I hope that answers your question :)

Cheers, Chris

Chris Kowalski
  • 876
  • 5
  • 4
  • 1
    Wow, this is super clever! I especially like the `exhaustiveCheck` that errors if one of the cases is missing! I guess the only difference is that calls to `onChange('id', 'def456')` need to be refactored to use `onChange({ key: 'id', value: 'def456' })` – styfle Apr 04 '18 at 20:46