Search code examples
typescripttypescript-generics

How to correctly fix TS2345 compilation error in TypeScript 4.7 to 4.8 upgrade


Our code was compiling and running fine under TypeScript 4.7.4, but we are now trying to upgrade from TypeScript 4.7.4 to a newer version. We are seeing an error when compiling on 4.8.4 that was not present when compiling with 4.7.4.

I have isolated the error into its own small code-snippet. Compiling with 4.8.4 (or any newer version of TypeScript) we get an error like this:

src/example2.ts:19:62 - error TS2345: Argument of type 'object' is not assignable to parameter of type 'Record<string, BasicValue>'.
  Index signature for type 'string' is missing in type '{}'.

19         typeof value === 'object' && areFieldsValid(PFields, value, isValidPField);
                                                                ~~~~~

This sounds similar to, but not the same as, a description of a breaking change in the release notes for TypeScript 4.8 - https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#unconstrained-generics-no-longer-assignable-to

Our reproducible example code looks like:

export interface PNode { 
    'text'?: string
}

export const PFields = ['text'];

export type DefinedBasicValue = number | boolean | string | Array<BasicValue> | {} | {
    [key: string]: BasicValue
    [key: number]: BasicValue
}

export type BasicValue = undefined | DefinedBasicValue


/**
 * Take any object, value, undefined, or null, and determine if it is a PNode
 */
export const isPNode = (value?: {}): value is PNode =>
        typeof value === 'object' && areFieldsValid(PFields, value, isValidPField)

export function areFieldsValid(fields: string[], value: Record<string, BasicValue>, ...validations: ((field: string, value: BasicValue) => boolean)[]): boolean {
    return true
}

export const isValidPField = (field: string, value: BasicValue): boolean => true

The error is with the value argument in the function call (on line 19): areFieldsValid(PFields, value, isValidPField).

Please can someone help us to understand why this is ok in TS 4.7, but not in newer versions? Also what would be the "best" / "correct" way to go about fixing such errors? We want to avoid "workarounds" please.

Playground using v4.7.4 with no error

Playground using v4.8.4 with the error

Playground with the current version (v5.4.5) with the error


Solution

  • VLAZ has explained the change in v4.8, but the typeguards they've shown don't prevent isPNode from passing primitives to areFieldsValid, which would make me uncomfortable.

    You've said you want to call it with any value, including primitives (which would make it return false). To my mind, that's a valid use of any, but you could also use unknown. I'm not bothered by using any in this situation because isPNode is a type predicate, presumably used at some kind of boundary between the untyped world and the typed world (otherwise you wouldn't need to allow primitives, since it will always return false for them), and you've said you literally want it to allow any value. But if you follow the "no explicit any" rule or work in a shop that does, you can use unknown or {} | null as well, they just require adding a type assertion.

    any:

    export const isPNode = (value?: any): value is PNode =>
        typeof value === 'object' &&
        !!value &&
        areFieldsValid(PFields, value, isValidPField);
    

    Playground link

    Note I added a && !!value to weed out null, since typeof null is "object").

    unknown:

    export const isPNode = (value?: unknown): value is PNode =>
        typeof value === 'object' &&
        !!value &&
        areFieldsValid(PFields, value as Record<string, BasicValue>, isValidPField);
    

    Playground link

    Again note the && !!value to handle null. I don't think you can avoid the type assertion there, though I could be mistaken. I'm still getting used to unknown.

    {} | null

    Finally, you could also use value?: {} | null, although I think that's basically just another way to write value?: any. The same implementation as the unknown solution works with that type as well:

    export const isPNode = (value?: {} | null): value is PNode =>
        typeof value === 'object' &&
        !!value &&
        areFieldsValid(PFields, value as Record<string, BasicValue>, isValidPField);
    

    Playground link