#Constraining a generic type to a union of string literals

41 messages · Page 1 of 1 (latest)

karmic mirage
#

Hello, I am trying to use a function that determines the type of an object's property "payload" based on an argument passed to it. I've attempted to constrain a generic type to a union of string literals but Typescript isn't happy about it. Certainly I'm missing something big here. Would appreciate help if anyone has time! Thanks ❤️ https://www.typescriptlang.org/play?#code/C4TwDgpgBAggxsAlgewHYGcoF4oG8qiQBcUA5GAE6JwSkA0UYAhiADbJMAmJqArgLYAjCBSgBfKAB8oAKHyEIJUgEdeTVElD1GLdlx4DhoidLkFwispybBaDZmw7co6YFVQBzcVADcMmXBorlC8YNa2AGKIEKyc2FAAPAAqUBAAHraonJjkVDSkUmSq6pogBdKk4bQAfDIAFAokSfa6Tk2pGRBZOVUFAPwuboieUAZCIgCU2NV4MlDzUIEYwFBMCCgYRPBIQfG4cwvzDnqcdAeHCmeHYn7zYkA

hearty cargoBOT
#

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

Mup#0501

Preview:```ts
type Actions =
| {type: "price"; payload: number}
| {type: "quantity"; payload: number}
| {type: "date"; payload: string}

const updateField = <
T extends "price" | "quantity" | "date"

(
type: T,
payload: T extends "date" ? string : number
) => {
const actions: Ac
...```

wintry widget
# karmic mirage Hello, I am trying to use a function that determines the type of an object's pro...

Type '{ payload: string | number; type: "price" | "quantity" | "date"; }' is not assignable to type 'Actions'.
Type '{ payload: string | number; type: "price" | "quantity" | "date"; }' is not assignable to type '{ type: "date"; payload: string; }'.
Types of property 'type' are incompatible.
Type '"price" | "quantity" | "date"' is not assignable to type '"date"'.
Type '"price"' is not assignable to type '"date"'.
'actions' is declared but its value is never read.

#

this is what the errors say

woven nymph
#

!:corr*

hearty cargoBOT
#
Retsam19#2505
`!retsam19:correspondence-problem`:

There's a particular pattern that is safe but hard for the Typescript compiler to handle, which I call the "correspondence problem":

const functionsWithArguments = [
  { func: (arg: string) => {}, arg: "foo" },
  { func: (arg: number) => {}, arg: 0 },
];

for (const { func, arg } of functionsWithArguments) {
  func(arg);
//     ^^^
// Argument of type 'string | number' is not assignable to parameter of type 'never'.
//   Type 'string' is not assignable to type 'never'.
}

The problem is that func is typed as (x: string) => void | (x: number) => void and arg is string | number, but the compiler can't prove that they "correspond": that, for example, arg is only a string when func accepts strings.

As far as the type are concerned, arg could be number, and func could be (arg: string) => void, and that would be a type-error. It's easy for us to see that that won't happen, but that requires understanding the program at a higher-level than the level the compiler operates.

Depending on the specifics there's sometimes clever fixes, but usually I recommend using a type assertion and ignoring the issue:

func(arg as never);
karmic mirage
# woven nymph !:corr*

Thank you, this is very interesting. I fixed it by asserting the type of the object. Can you explain why asserting arg as never works in this example linked?

woven nymph
karmic mirage
#

Thanks, I appreciate your help!

#

!resolved

woven nymph
#

note that in many cases as any makes it clearer to most people that it's just a way to suppress the error

#

technically as never is a tiny bit safer though since you can't do property accesses on it

#

but realistically a lot, if not most of the time you're doing as never/as any in a return statement, or in a function parameter, where you couldn't access a property even if you tried

karmic mirage
woven nymph
#

you can

karmic mirage
#

I can't check rn but I'm just seeing that the error is // Argument of type 'string | number' is not assignable to parameter of type 'never'. // Type 'string' is not assignable to type 'never'.

woven nymph
#

wait

#

what are you doing

woven nymph
# woven nymph you can

as any is basically saying "pretend this is unknown or object or never, whichever gives feweest errors"

#

ok not really

#

it just means "suppress all type errors related to this variable"

karmic mirage
# hearty cargo

Ah, but I mean in this example it seems you're forced to cast it as never.

woven nymph
#

ah, indeed

#

i'm guessing that's a special case, and that only happens when the argument's type is never

karmic mirage
#

Okay, last question - why is the argument inferred as never here in the first place?

hearty cargoBOT
woven nymph
#

!ts

hearty cargoBOT
#
let x!: any;
((x: any)=>{})(x);
((x: unknown)=>{})(x);
((x: undefined)=>{})(x);
((x: 1|2)=>{})(x);
(<T,>(x: T)=>{})(x);
((x: never)=>{})(x);
//               ^
// Argument of type 'any' is not assignable to parameter of type 'never'.```
woven nymph
#

because we don't know which one it is

#

and it's pretty much impossible to tell functions apart

#

and so the only valid input for arg must be a string and a number at the same time

#

which, of course, doesn't exist - aka never

woven nymph
karmic mirage
woven nymph
#

yup

karmic mirage
#

Okay, you're awesome. Thank you!

woven nymph
hearty cargoBOT
woven nymph
#

!ts

hearty cargoBOT
#
const functionsWithArguments = [
  { func: (arg: string) => {}, arg: "foo", type: "string" as const },
  { func: (arg: number) => {}, arg: 0, type: "number" as const },
];

for (const { func, arg, type } of functionsWithArguments) {
    switch (type) {
        case "string": { func(arg); break; }
//                       ^? - const func: (arg: string) => void
        case "number": { func(arg); break; }
//                       ^? - const func: (arg: number) => void
    }
}```