3

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?

2
  • Which version of TypeScript are you using? New versions introduced some stricter generics checking. Commented Oct 26, 2017 at 15:34
  • 1
    Typescript 2.5.3 Commented Oct 26, 2017 at 15:37

1 Answer 1

2

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!

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks @jcalz, great answer. I reached the same conclusion eventually but couldn't figure out the discrepancy. I'm only a few hours into my first Typescript project, and have hit edge cases like this-- some seem to be bugs, others are just naive implementations on my end. This helps a lot, thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.