#why TS don't accept this as a number?

55 messages · Page 1 of 1 (latest)

violet ravenBOT
#
nnry#0

Preview:```ts
function update<T extends Record<string, number>>(
data: T,
key: keyof T
): T {
if (typeof data[key] === "number") {
data[key] += 1
}
return data
}

update({a: 2}, "a")```

granite panther
#

like the error message says, T[keyof T] could be a subtype of number. for instance:

violet ravenBOT
#
mkantor#0

Preview:```ts
function update<T extends Record<string, number>>(
data: T,
key: keyof T
): T {
if (typeof data[key] === "number") {
data[key] += 1
}
return data
}

const x = update<{a: 2}>({a: 2}, "a")
// ^?
console.log(x.a)
// ^?```

granite panther
#

that gives you back a value where x.a is typed as 2 but is actually 3 at runtime

grim helm
#

how would i get it to work the way that I want?

#

like how would i constrain T to not be a subtype of number

#

but still a number in which i can do operations on

granite panther
#

those two statements are contradictory. are you sure you actually want T[keyof T] to be allowed to be a literal number type?

grim helm
#

thats all i care about really, it can't be this difficult unless im misunderstanding something

granite panther
#

what do you want the return type to be if the argument type is something like { a: 2 }? is { a: number } okay? or do you need { a: 3 }?

grim helm
granite panther
#

did you understand my previous example?

#

TS has a concept called "literal types"

#

2 is a type, for example

grim helm
#

yea, you're saying something can be literally 2, in which case it's an error to do operations on it

granite panther
#
const x: 2 = 2
x++ // this is an error
#

right

grim helm
#

but in other languages, you're blocked from this simply because of the const

granite panther
#

but i think we can come up with a different type signature that doesn't allow subtypes like that

#

can you show me how you want to use this? i'm curious what you need to do with other properties that aren't key (if anything)

granite panther
grim helm
#

update({a: 3}, 'a') => {a:4}

granite panther
grim helm
#

sure that can work also

granite panther
#

and do you literally need to update it in place? you're mutating the argument and also returning the same thing you updated, but i'm not sure if that's intentional or not

grim helm
#

yea its intentional

#

i guess the problem is there's no way for TS to differentiate between a literal 2 and a number 2

#

is that the heart of it?

granite panther
#

it's more general than that: whenever you have a generic function the caller is allowed to be arbitrarily precise. you can't say "number is the lower bound"

#

this might be too general, but it's simple:

violet ravenBOT
#
mkantor#0

Preview:```ts
function update(
data: Record<PropertyKey, number>,
key: PropertyKey
): Record<PropertyKey, number> {
if (typeof data[key] === "number") {
data[key] += 1
}
return data
}

const x = update({a: 2, z: 999} as const, "a")
console.log(x.a)
...```

granite panther
#

let me know if that doesn't work and i'll see if i can come up with something better

grim helm
#

couldn't they have added a type Literal<number> and once its declared, it literally means a literal, and won't ever be confused with a number?

granite panther
#

i don't understand what that means. you want number, not anything more specific, right?

#

2 should definitely be assignable to number. if you're saying there shouldn't be a subtype relationship there then i disagree

grim helm
#

however if i dont type it, and just pass in a generic {a : 2}, it will assume number

granite panther
#

you'd need some way to express lower bounds (or maybe it'd be upper bounds? i'd need to think about it more). A extends B means "A can be any subtype of B". there'd need to be syntax like A extends B upto C or something which says "A can be any subtype of B that is not narrower than C"

#

the fact that you want to mutate the argument in-place makes this trickier, because you can't decouple the argument type from the return type

#

what's the reason to both update in-place and return it? that feels a bit redundant

grim helm
#

it's just a contrived example

granite panther
#

can you share some info about the real use case?

grim helm
#

its a problem i saw someone else have, so i just made up the simplest example i could to be a "number" even with narrowing it, i couldn't understand the error msg because i couldn't think what could be a subtype of a number... but now i know

#

what you explained makes sense, but practically speaking i don't think it makes sense, and I'm trying to think of another language where you'd run into the same issue.... and i'm more assuming this is because of TS being limited in what it can / can't do

granite panther
#

you have similar situations in any language with generic functions

#

the specific thing with literal number types is maybe because TS is more powerful. other languages tend to "bottom out" at primitive types, but TS's structural type system means you can keep on subtyping forever

#

but without the constraint this same sort of thing can happen in Java or whatever

grim helm
#

well rust has generics, but this won't happen, is it because you can't inherit/extend things

granite panther
#

correct, rust doesn't have subtyping at all IIRC

#

(well i guess lifetimes can be subtyped kinda? but not regular value types)

grim helm
#

ok i'll have to mull over it some more, because 2 having a relationship with number makes sense, but it also doesn't make sense since it feels more like a symbol in practical usage?

#

well anyway, thank you @granite panther

#

!close

violet ravenBOT
#
sandiford#0

Preview:```ts
const o = {
a: 2,
b: 4,
c: "foo" as const,
}

const o2 = {
a: 2 as const,
b: 4 as const,
c: "foo as const",
}

function update<
T extends {[K in Key]: number},
Key extends string

(data: T, key: Key) {
data[key]++
return data
...```