What I'm trying to do is this:
interface A {
a: number
b: number
}
function f<T extends A>() {
const x: Partial<Record<keyof T, string>> = {a: 'generz'}
console.log(x)
}
But, when compiling (using tsc v4.9.3
) I get this error message:
error TS2322: Type '{ a: "generz"; }' is not assignable to type 'Partial<Record<keyof T, string>>'.
I don't understand why and I would like to have an explanation on this error. If T extends A
then keyof T
is a superset of keyof A
(containing at least 'a'|'b'
), so the {a: 'generz'}
would be legal independently of T
? Am i missing something?
I've found out, that creating a custom PartialRecord
type (described here) like this:
type PartialRecord<K extends keyof any, T> = {
[P in K]?: T
}
And then changing the type of variable x
to PartialRecord<keyof T, string>
, like this:
const x: PartialRecord<keyof T, string> = {a: 'generz'}
Compiles the code without complaining.
Update 2022-11-25
This would be equivalent code and it compiles too:
const x: Partial<Record<keyof T, string>> = {}
x.a = 'generz'
Although is not what i'll like to do.
Update 2022-11-29
What I'm asking is NOT "how to make it work in alternative ways", but is "why it doesn't work that way", since I expect it to work. Otherwise I'd just do {a: 'generz'} as Partial<Record<keyof T, string>>
(or worse as any
).
And if it wasn't clear: the code is nothing more than the minimum necessary to reproduce the error. So its purpose is not to make sense or do anything useful.
CodePudding user response:
Nesting a Record<keyof T, string>
inside a Partial
leads to a level of abstraction which can not be properly understood by the compiler. Both Record
and Partial
are implemented as mapped types which can not be fully resolved when a yet unspecified generic type is supplied.
While the logic behind your assignment might be sound, it requires higher level reasoning by the compiler about the implications of nesting Record<keyof T, string>
inside a Partial
. Yet, it seems to be inable reason about this type at all leaving it essentially opaque. No expression would identify this type as a valid assignment target leaving only a type assertion as a workaround.
There have been multiple open issues about these higher order type problems like #43846 where Partial
being wrapped around Omit
lead to the same error or #28884 where an intersection of
Pick<T, Exclude<keyof T, K>> & Pick<T, Extract<keyof T, K>>
was not assignable to T
. The compiler would fail to properly evalaute the generic types leaving them opaque, even though these operations a reasonable and mostly sound to us.
CodePudding user response:
Update 2
Lets pretend to use the function.
function f<T extends A>() {
const x: Partial<Record<keyof T, string>> = { a: 'generz' }
// Type `{ a: "generz"; }` is not assignable to...
return x
}
const a = f<
{
a: number
b: number
} & {
c: () => number
}
>()
if (a.c) doWithString(a.c)
doWithString
will never run because a.c
will ALWAYS be undefined. The property is not assigned to x
at creation and types are immutable. So even if you intend to assign the other keys a string value later in the function, the annotation is correctly alerting now that x
is not what you said it should be.
c
is always undefined inx
x
narrows to a type withoutc
c
is akeyof T
- every
keyof T
should be akeyof x
- ERROR
x
does not match your annotation.
I don't really know how else to say it. Just don't think of extends
as equal
, and don't use annotations as if you were extending a class.
Also, based on your edits/commments, I recommend you check out the differences between type annotations and type assertions. Everything below exemplifies how one might actually achieve what your function appears it would like to do. The final example specifically expects you to conditionally assign the values somewhere in the body of the map/reduce callback functions.
Update - Typescript 4.9
Using the new satisfies
operator, you gain better type inference without assertions.
function fn<T extends A>() {
const x = {a: 'generz'} satisfies {[K in keyof T]?: string }
return x
}
const x = fn()
/* {
a: string;
} */
Original
interface A {
a: number
b: number
}
You've typed x
as a Record<keyof T, ...>
, but you never actually do anything with T
. You've just created a Partial<Record<keyof A, ...>
which happens to extend T
, but is completely irrelevant in implementation.
function fn<T extends A>( /* normally we use the parameter here */ ) {}
If this is what you want to do, simply remove the type parameter because its unnecessary. You can just return a Partial<A>
or your modified A
, Partial<Record<Keyof A, ...>>
.
function createA() {
const a: Partial<Record<keyof A, string>> = {a: 'generz'}
return a
}
A more realistic function might look like this. Here the type of t
, T
, must have AT LEAST all properties of A
, but maybe more. Then we create a new partial x
by mapping over the keys of T
(which would include keys of A
) and define their values as string
s.
function createB<T extends A>(t?: T) {
const x: { [K in keyof T]?: string } = { a: 'generz' }
return x
}
const b = createB({
a: 1,
b: 2,
c: 3,
})
// the input object's values were numbers.
// now they are strings | undefined.
/* {
a?: string | undefined;
b?: string | undefined;
c?: string | undefined
} */
const b2 = createB()
// t was not given.
// b2 does not include any additional properties,
// only the known properties of A.
/* {
a?: string | undefined;
b?: string | undefined;
} */
Extend/overwrite the functionality of t
safely.
interface C {
a: () => void
}
function createC<T extends A>(t: T) {
const x: {
[K in keyof C]?: C[K]
} & {
[K in Exclude<keyof T, keyof C>]?: string
} = {
...Object.entries(t)
.map(([k, v]) => {
return [k, 'generz'] as [keyof T, string]
})
.reduce((acc, [key, str]) => {
acc[key] = str
return acc
}, {} as { [K in keyof T]?: string }),
a: () => console.log('rotunda')
}
// consolidate the intersection into a more readable type.
return x as typeof x extends infer U ? { [K in keyof U]: U[K] } : never
}
const c = createC({
a: 1,
b: 2
})
// c "maybe" has all property keys of `T`,
// but all property values are type `string`,
// and if `T` had a property,`K`, of `C`, it has been replaced by `C[K]`
/* {
a?: (() => void) | undefined;
b?: string | undefined;
} */
c.a?.() // rotunda