0

Given the type,

export type ValidationErrors<T extends string> = Partial<Record<T, string>> & { errorsCount: number };

You can create an object like so:

const errors: ValidationErrors<'hello' | 'goodbye'> = {
  errorsCount: 0,
  hello: 'Hello',
}

However, when ValidationErrors is used with a generic parameter, I get the error Type '{ errorsCount: number; }' is not assignable to type 'ValidationErrors<T>'.. How can I fix this?

Here's an example:

const doSomething = <T extends string>() => {
  // Type '{ errorsCount: number; }' is not assignable to type 'ValidationErrors<T>'.
  const errors: ValidationErrors<T> = {
    errorsCount: 0,
  }

  return errors
}

const abc = doSomething<'hello'>()

abc.errorsCount // number
abc.hello // string | undefined

Playground

user82395214
  • 829
  • 14
  • 37

1 Answers1

2

This is because your intersection type conflicts with each other. You try to intersect an object which can be accessed with string, and tell it returns string, and yet errorCounts instead returns a number. This type-safety is to prevent a user from doing something like this

const abc = doSomething<'errorsCount'>()
abc.errorsCount //=> never

The simple solution is to cast it.

const doSomething = <T extends string>(): ValidationErrors<T> => {
  const errors = {
    errorsCount: 0,
  } as ValidationErrors<T>

  return errors
}

The longer answer is because generics are used to augment function parameters, as such your doSomething function has no parameters, and since all types are erased at compile time, it gives you this error since it will have no idea if it can guarantee the type of T as being as intended. The preferred way for representing this behavior is to do something like this.

export type ValidationErrors2={
  [index: string]: string | number
  errorsCount: number;
}

const doSomething2 = (): ValidationErrors2 => {
  const errors: ValidationErrors2 = {
    errorsCount: 0,
  }

  return errors
}

Here is a similar example of what you tried to do originally, and this error message gets at the crux of what TypeScript is doing here.

export type ValidationErrors3<T extends Object> = T & {
  errorsCount: number;
}

const doSomething3 = <T extends Record<Exclude<string, 'errorsCount'>, string>>(): ValidationErrors3<T> => {
  //error VVV
  const errors: ValidationErrors3<T> = {
    errorsCount: 0,
  }

  return errors
}

Type '{ errorsCount: number; }' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to '{ errorsCount: number; }'. However, this goes beyond the scope of your question probably. Read more specifically about Object types and subtypes and how this contributes to the issue you face How to fix TS2322: "could be instantiated with a different subtype of constraint 'object'"?

Another answer specifically related to your question, in this case it describes the multiple ways to go about typing a intersection like yours.

Cody Duong
  • 2,292
  • 4
  • 18
  • Makes sense, thank you. Is there no way to constrain the string generic T to all strings except for `errorCount`? Something like... ``? – user82395214 Mar 23 '22 at 13:36
  • 1
    @user82395214 There is the `Exclude` utility type, however this does not work on `string` unless it a specific type of string. IE. `Exclude` is still `string`, while `Exclude<'foo' | 'test', 'test>` is `foo`, see here: https://github.com/microsoft/TypeScript/issues/47178. Basically TS prefers you know all your keys ahead of time in order to do this sort of thing. – Cody Duong Mar 23 '22 at 16:14