#Force `map` output to match the inferred type from input

34 messages · Page 1 of 1 (latest)

lean robinBOT
#

@visual lantern Here's a shortened URL of your playground link! You can remove the full link from your message.

thefrenchpoulp#0

Preview:```ts
const process = <
TCrumbs extends readonly [string, ...string[]]

(
crumbs: TCrumbs
) => crumbs

const processWithPretty = <
TCrumbs extends readonly [string, ...string[]]

(
crumbs: TCrumbs
) => crumbs.map(crumb => crumb.replace(/^\d+-/, ""))

const input: [a: string, b: string] = ["foo", "bar"]
...```

hasty fable
#

@visual lantern I don't think there's any way to make this happen automatically, but you can cast:

const processWithPretty = <TCrumbs extends readonly [string, ...string[]]>(crumbs: TCrumbs) =>
  crumbs.map((crumb) => crumb.replace(/^\d+-/, '')) as {[K in keyof TCrumbs]: string};
lean robinBOT
#
retsam19#0

Preview:```ts
const process = <
TCrumbs extends readonly [string, ...string[]]

(
crumbs: TCrumbs
) => crumbs

const processWithPretty = <
TCrumbs extends readonly [string, ...string[]]

(
crumbs: TCrumbs
) =>
crumbs.map(crumb => crumb.replace(/^\d+-/, "")) as {
[K in keyof TCrumbs]: string
}
...```

visual lantern
#

What sorcery is this? Does as simply helps inferring what's in front? I've always thought it was casting input into output

#

Because the way I read this is as telling TypeScript that the array is an object literal

exotic basalt
#

it is a cast

hasty fable
#

It's a cast - it's just that you can use mapped type syntax with arrays/tuples too

#

It's kinda a special case built into the compiler.

#

But arrays are objects, so it's not that special of a case.

visual lantern
#

you can use mapped type syntax with arrays/tuples too
Oh right of course

#

Now that I know more and that future me might even understand immediately, which of the 2 is the more natural to read/better to find in a shared codebase?

  • as unknown as TCrumbs
  • as { ... }
#

I realize I'm asking an opinion, but since I'll likely never touch that code ever again other than by reading the type annotations it'll give me from where I consume it, I'm wondering which is the more customary for a developer new to the codebase who might read it as well

exotic basalt
#

as unknown as TCrumbs is incorrect

hasty fable
#

TCrumbs could be a type like ["foo", "bar"]

#

Your output isn't ["foo", "bar"] which is what as TCrumbs would claim.

lean robinBOT
#
that_guy977#0

Preview:```ts
const process = <
TCrumbs extends readonly [string, ...string[]]

(
crumbs: TCrumbs
) => crumbs

const processWithPretty = <
TCrumbs extends readonly [string, ...string[]]

(
crumbs: TCrumbs
) =>
crumbs.map(crumb =>
crumb.replace(/^\d+-/, "")
) as unknown as TCrumbs
...```

exotic basalt
#

as <unknown/never/any> as T is very unsafe, so avoid it if possible

#

as T still has a sanity check with disjoint types

visual lantern
#

Ah yes, didn't think of literal types good point
Thanks
Is there a specific reason that map, reduce etc typically dumb down the input type, even when it's readonly like in this case?
For instance, is there a correct usage of map that can lead to anything more loose than [string, string] when the input type is [string, string]?

#

Obviously I'm not referring to usage where the generic output is customized ie. map<TOutput>(...)

exotic basalt
#

they just weren't written to handle tuples. i think it was written before mapping tuples was possible

#

but filter would have to use the element type, since the length of the result wouldn't be the same

#

and not sure what you mean with reduce

visual lantern
#

Reduce was a bad example, it's generally difficult to tell reduce what to do
Mostly a lack of practice, probably

hasty fable
#

If you want a tuple-aware map you can write one:

lean robinBOT
#
function mapTuple<const T extends readonly unknown[], U>(input: T, mapper: (val: T[number]) => U) {
  return input.map(mapper) as { [K in keyof T]: U};
}

const input = ["a", "b"] as const;
const output = mapTuple(input, x => x.length);
//    ^? - const output: readonly [number, number]
exotic basalt
#

it can't do transforms per element though, that would require higher kinded types

hasty fable
#

Yeah, it basically only preserves the length.

lean robinBOT
#
function mapTuple<const T extends readonly unknown[], U>(input: T, mapper: (val: T[number]) => U) {
  return input.map(mapper) as { [K in keyof T]: U };
}
const input = ["a", 0] as const;
const output = mapTuple(input, x => typeof x === 'string' ? x.length : x.toString());
//    ^? - const output: readonly [string | number, string | number]
hasty fable
#

As for why the existing API isn't tuple-aware, 1) I'm not sure how you'd actually write that, given that the types are like:

interface ReadonlyArray<T> {
    map<U>(fn: (x: T) => U): U[]
}

there isn't really a way you could change that that to do tuple mapping. There isn't any way the map signature can get at whether or not the thing being mapped is a tuple, AFAIK.

#

And 2) even if you could, making every map operation involve a mapped generic type would probably slow compile times for something that isn't a very common need

visual lantern
#

Fair enough

#

Thanks a lot for the answers, as always, learned a lot quickly