Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - type mapping fails inside a generic function

Given these Typescript types:

// Validation types
type Methods = 'firstName' | 'lastName' | 'email'
type Method<T> = {[K in keyof T]?: Methods }
type Row<T> = keyof T | Method<T>
type Mapping<T> = Array<Row<T>>

// Fields that will be accepted
type Fields = {
  firstName: string
  lastname: string
  e?: string
}

And this data:

// Data object, holding the form submission
const data: Fields = {
  firstName: 'John',
  lastname: 'Doe',
  e: '[email protected]'
}

// Map validation methods to the data fields
const mapping: Mapping<Fields> = [
  'firstName',
  'lastname',
  {
    e: 'email'
  }
]

Why does this work:

const validationFuncs: Method<Fields>[] = mapping.map(m => {
  return typeof m === 'string' ? { [m]: m } : m;
})

// validationFuncs[0].firstName // works

But this doesn't?

function validate<T>(mapping: Mapping<T>) {
  const validationFuncs: Method<T>[] = mapping.map(m => {
    return typeof m === 'string' ? { [m]: m } : m;
  })
}

Why?

like image 816
Lee Benson Avatar asked Jul 01 '26 21:07

Lee Benson


1 Answers

The values of Method<T> must be Methods, which can only be "firstName", "lastName", or "email". In your generic example:

function validate<T>(mapping: Mapping<T>) {
  const validationFuncs: Method<T>[] = mapping.map(m => {
    return typeof m === 'string' ? { [m]: m } : m;
  })
}

the type T can be anything... for example, {nope: string}. In this case, keyof T is "nope", and the declared type of validationFuncs is {nope?: Methods}[].

But if mapping is ["nope"] (a valid Mapping<{nope: string}>), then validationFuncs will be [{nope: "nope"}] at runtime. But that's not a {nope?: Methods}[], because validationFuncs[0].nope is "nope" instead of undefined or any of the three allowable values for Methods. So the compiler warns you about that. This all makes sense to me.


In your non-generic, "working" example:

const validationFuncs: Method<Fields>[] = mapping.map(m => {
  return typeof m === 'string' ? { [m]: m } : m;
})

something weird is happening. Method<Fields> is equivalent to

type MethodFields = { 
  firstName?: Methods
  lastname?: Methods
  e?: Methods 
}

But the type checking of { [m]: m } isn't working properly because of a TypeScript bug with computed keys which might be fixed in TypeScript 2.6; not sure.

The compiler should (but does not) realize that { [m]: m } is only guaranteed to be {firstName:"firstName"}, {lastname:"lastname"}, or {e:"e"}, the last two of which are not valid Method<Fields> elements (notice the lowercase "n" in lastname). Instead, the type checker widens the type of { [m]: m } to something like { [k: string]: keyof Fields }, which is apparently wide enough to match Method<Fields>, not that I understand how. Anyway, it type checks when it shouldn't; it's a bug or a design limitation in TypeScript.


In both cases you're not implementing your code in such a way to conform with the types you've declared. I can't tell whether your types are right but the implementation is wrong, or vice versa, or something else. I hope you have enough information now to make progress. Good luck!

like image 152
jcalz Avatar answered Jul 06 '26 07:07

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!