Given a variable v
of generic type T extends { a: string }
, how can we narrow it so that v['count']
is typed as number
AND we can call setProperty(v, 'count', 1)
?
function maybeSetCountToOne<T extends {a: string}>(v: T) {
if ('count' in v && typeof (v.count) === 'number') {
v.count = 1 // works (but v.count is unknown)
setProperty(v, 'count', 1) // fails because 'T["count"]' could be unrelated to 'number'
}
}
function setProperty<
T extends {a: string},
K extends keyof T
>(o: T, k: K, v: T[K]) {
o[k] = v
console.log(o.a)
}
All the classic solutions I can think of lead more or less to v
typed as T & Record<'count', number>
, which doesn't make setProperty(v, 'count', 1)
a valid call because T
may have a more precise type than number
for the count
key.
You can cast the type of the passed number to be (T & {count: number})['count']
, essentially narrowing it to a type that's yet to be known, but compatible with T
.
And, since we know, from the comments on the question, that at runtime we don't need to narrow v.count
further than number
, we use a type guard to assert that v
extends {count: number}
.
interface Entity {
a: string
}
function countIsNumber<T extends Entity>(x: T): x is T & {count: number} {
return 'count' in x && typeof x.count === 'number';
}
function setProperty<T extends Entity & Record<K, unknown>, K extends keyof T>(o: T, k: K, v: T[K]) {
o[k] = v
}
function maybeSetCountToOne<T extends Entity>(v: T) {
type ParamCount = (T & {count: number})['count'];
if (countIsNumber(v)) {
v.count = 1 // v.count: number - Thanks to the type gaurd
setProperty(v, 'count', 1 as ParamCount) // No error, thanks to the type cast
}
}
If we want to avoid type casts. We can use another type guard that asserts that the value we're passing is the same type as v['count']
:
interface Entity {
a: string
}
function countIsNumber<T extends Entity>(x: T): x is T & { count: number } {
return 'count' in x && typeof x.count === 'number';
}
function countFitsObj<V extends { count: unknown }>(x: V, y: unknown): y is V['count'] {
return typeof y === typeof x.count;
}
function setProperty<T extends Entity & Record<K, unknown>, K extends keyof T>(o: T, k: K, v: T[K]) {
o[k] = v
}
function maybeSetCountToOne<T extends Entity>(v: T) {
if (countIsNumber(v)) {
const newCount = 5;
if (countFitsObj(v, newCount)) {
setProperty(v, 'count', newCount) // No error, no typecast
}
}
}