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
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);
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);
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);