2

I'm trying to figure out how to correctly type a show function that would take an object T and a key K for which T[K] can be guaranteed to have a toString() method implemented.

Here is my attempt using mapped types

type ToStringablePropertyKeys<T> = keyof {
    [K in keyof T]: { toString(): string }
}

function show<T, K extends ToStringablePropertyKeys<T>>(t: T, k: K): string {
    return t[k].toString()
}

But the compiler says Property 'toString' does not exist on type 'T[K]'.

What am I missing here? How do I convince tsc that toString is in fact there by definition of K?

francoisr
  • 4,407
  • 1
  • 28
  • 48
  • How is that useful? Every primitive value and object has a `toString` method in JavaScript. – Clashsoft Jun 22 '21 at 14:18
  • Since the compiler complains about `toString` not being there, I'm assuming there are valid properties that don't have `toString`. Either way, I might want to apply the same pattern to something less common than `toString`: what if I want to guarantee that a `T[K]` is of a given type `V`? – francoisr Jun 22 '21 at 14:42
  • @Clashsoft `null` doesn't have a `toString()`... nor `undefined`. – Andrei Tătar Jun 22 '21 at 14:58
  • Please see [the answer](https://stackoverflow.com/a/61765012/2887218) to the question this duplicates for more info; translating that code here yields [this](https://tsplay.dev/wQAojw)... good luck! – jcalz Jun 22 '21 at 14:59

2 Answers2

6

An alternative approach:

function show<K extends string, T extends {[key in K]:{toString(): string}}>
(t: T, k: K ): string {
return t[k].toString()
}
Nadia Chibrikova
  • 4,916
  • 1
  • 15
  • 17
2

Here's an almost ideal solution. Though typescript can enforce the keys to be only the ones that expand on {toString():string}, it's not smart enough to infer that from there on, all T[k] will have toString():string...

type ToStringable = { toString(): string };

type ToStringablePropertyKeys<T> = {
    [k in keyof T]: T[k] extends ToStringable ? k : never
}[keyof T];

function show<T, K extends ToStringablePropertyKeys<T>>(t: T, k: K): string {
    return (t[k] as any).toString(); // it seems typescript is not smart enough to infer that all t[k] will have toString
}

const testObject = {
    string: 'test',
    number: 123,
    null: null,
    undefined: undefined,
};

show(testObject, 'number');
show(testObject, 'string');
show(testObject, 'invalid-prop'); //this fails
show(testObject, 'null'); //this fails
show(testObject, 'undefined'); //this fails

https://www.typescriptlang.org/play?ts=4.3.4#code/C4TwDgpgBAKg9gZWAJwJYDsDmBDARgG2gF4oBvKYRFDTACgEoAuKAZ2qygF8BuAKF9CRYVNFjyEACsjiRkoANIQQLADwwAfFBKleUPVADaAaygYoRpXABmsALrMYx21AgAPYBHQATFsKSicAmgAfnMoZnQIADcIZF5OY0sbGFs+XisAV3QAY2BUOHRWAAs4AHc1ABooeRd3Tx8-dkDJaVkFJVUNdVpgByqjZnkmViayXX1kCGAM5EKepyhsX2x0EHoAOkp-GgZuKAB6fdNgVggIAFtfQQgWbLQwE9RfdDgTlnPsORcXjMwiijgpnQVliFCK2BO2Hw+AoC1KqGhUHBMQB2yw8X42QKbAoN2AAHlcAArCC5LRjfQjALMADkHjYNIq4z06Ay51wsWYAEYAEwAZiZlNZ0IiGWhgv0WS8ECsGAgXmYUplcq8TJ4-BYJVKPTxhJJuSqNNZ7NiNPofE1ZR1bD1pOAhrYATNFq11oJxLthowUShqC8AFowK1nQd9sAik8oFZsAiWLxLdr6e79faoEaxfgQ4dw5Ho7H466k7aDWmlbLIl4s2GI748-g40A

Andrei Tătar
  • 7,872
  • 19
  • 37