Alex Craft
Alex Craft

Reputation: 15356

TypeScript, exhaustiveness check not working properly

Let's say we are writing DB model for Post, and because database stores everything as string we need to write parse function that would take raw DB object and cast it to proper Post interface.

To reproduce set noImplicitReturns: true

interface Post {
  id:   number
  text: string
}

function parse<K extends keyof Post>(k: K, v: any): Post[K] {
  switch(k) {
    case 'id':   return parseInt(v)
    case 'text': return v.toString()
  }
}

There are two errors in that code. First - it won't compile, it seems like TypeScript didn't realises that our code is correct and ask for default statement to be added to the switch.

The second error - it won't detect that you are checking against wrong value. The wrong code below would compile without errors

function parse<K extends keyof Post>(k: K, v: any): Post[K] {
  switch(k) {
    case 'id':   return parseInt(v)
    case 'text': return v.toString()
    case 'some': return v.toString() // <= error, no `some` key
    default: return ''               // <= this line is not needed
  }
}

There's even third error, TypeScript would allow to return wrong value for the key, this wrong code would compile

function parse<K extends keyof Post>(k: K, v: any): Post[K] {
  switch(k) {
    case 'id':   return parseInt(v)
    case 'text': return 2 // <= error, it should be string, not number
    default: return ''
  }
}

Are those TypeScript limitations or I'm dong something wrong?

Upvotes: 2

Views: 275

Answers (2)

Nandin Borjigin
Nandin Borjigin

Reputation: 2154

Would this alternative appeals to you ?

interface Post {
  id:   number
  text: string
}

type Parsers = {
    [k in keyof Post]: (v: any) => Post[k]
}

const parsers: Parsers = {
    id (v: any) {
        return parseInt(v)
    },
    text (v: any) {
        return v.toString(v)
    }
}

parsers.id('123') // number
parsers.text({}) // string

// In addition, adding extra parsers or returning a value of wrong type from a parser is type-checked.

Update

Making Parser(renamed from Parsers) type declaration generic and exploiting arrow function and contextual type inference, the code can be improved to:

type Parser<T> = {
    [k in keyof T]: (v: any) => T[k]
}

interface Post {
  id:   number
  text: string
}

const parser: Parser<Post> = {
    id: v => parseInt(v),
    text: v => v.toString()
}

Upvotes: 1

Nguyen Phong Thien
Nguyen Phong Thien

Reputation: 3387

Firstly, K extends keyof Post doesn't mean K is keyof Post, so instead of using generic type K, you must use keyof Post to limit the potential values of K.

Secondly, the default option is forced if you set "switch-default": true, in your tsconfig.json. So set it to false then the issue will go away.

Thirdly, Post[K] or even Post[keyof Post] will return all the possible types of Post properties, so number is possible. Typescript doesn't force Post[K] to the type of its K property, unless you point out which K is. Otherwise you must define the mapping type, like type A = ['id', numer] | [text', string] and define Post as [A[0]]: A[1]

Hope this help

Upvotes: 2

Related Questions