1

Probably been asked and answered but I didn't find the answer - so I ask.

The workaround is not great as if I want to include more type it will not be as easy to upgrade. Any better way of doing it ?

declare const max: <A extends number | string> (a: A) => (b: A) => A

max (2) (3)     // Argument of type '3' is not assignable to parameter of type '2'
max ('a') ('b') // Argument of type '"b"' is not assignable to parameter of type '"a"'

// Possible workaround but will become clumsy if possible type to extends grow

declare const maxClumsy: {
    (a: number): (b: number) => number
    (a: string): (b: string) => string
}

maxClumsy (2) (3)
maxClumsy ('a') ('b')

playground link

zedryas
  • 718
  • 6
  • 19
  • 1
    Just want to note that another option is to specify the generic type whenever you call the function (e.g. `max(2) (3)`), but I realize that may not be ideal. – Matt Diamond Feb 11 '22 at 23:44
  • 1
    this question seems related: https://stackoverflow.com/q/67070250/359878 – Matt Diamond Feb 11 '22 at 23:46
  • 2
    Keep in mind that you're passing in literals here... if the input variables are typed more broadly, you won't have an issue. See here, for example: https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEYD2A7AzgF3gWygDwC54AeAQXhDwxBWDXhQFdsAjEGeAH3kxgEsUAcwB88ABRQipAJTwAvKLEspshfFIBYAFDbk6LJIbM2HOfABMu1JnjKjrdvPgBmbVf2IivAYKcByKD93G2AvDH4hfxYgnS1cPHEoWSVpbXjxMGTgaSA – Matt Diamond Feb 11 '22 at 23:54
  • How many types could be relevant in a `max` function anyway? I bet that list is pretty small. – Alex Wayne Feb 12 '22 at 01:07

2 Answers2

2

Function overloads are the way to go here, I say. For just two types, your declaration seems fine:

declare const maxClumsy: {
    (a: number): (b: number) => number
    (a: string): (b: string) => string
}

But if you really are concerned with the repetition of string and number, then you can construct this type programmatically; function overloads are equivalent to an intersection of function types, so we can use a distributive conditional type and the UnionToIntersection helper from this other Q&A to transform the union type string | number into the desired type:

type CurriedUnion<T> = T extends any ? (a: T) => (b: T) => T : never
type UnionToIntersection<T> = (T extends any ? (a: T) => void : never) extends (a: infer U) => void ? U : never
type CurriedOverload<T> = UnionToIntersection<CurriedUnion<T>>

Usage:

// type Test = ((a: string) => (b: string) => string) & ((a: number) => (b: number) => number)
type Test = CurriedOverload<string | number>

declare const coolMax: CurriedOverload<string | number>

// OK
coolMax (2) (3)
// OK
coolMax ('a') ('b')
// error
coolMax (2) ('b')
// error
coolMax ('a') (3)

Playground Link

Be forewarned that distributing over union types like this can cause unexpected results when the input types are themselves unions; particularly, boolean is defined as the union type true | false, so this will not do what you want in that case.

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • thanks a lot - could you explain why you need to check if `T extends any` is necessary in the definition of `CurriedUnion` ? – zedryas Feb 12 '22 at 01:08
  • 1
    That's what makes it a distributive conditional type; please see the documentation link. – kaya3 Feb 12 '22 at 01:11
  • It works like a charm, but breaks for `boolean` @kaya 3 any workaround ? [link](https://tsplay.dev/WvVzQW) – zedryas Feb 12 '22 at 01:25
  • 1
    Try: [Playground Link](https://www.typescriptlang.org/play?#code/C4TwDgpgBAwgrgJwQSwgEwKoDtkHssA8AKlBAB7ARZoDOUAhliANoC6AfFALxQnmXU6zZFgBmEBFACCAGigA6RSPGSAQqygB+KAAod9AFzSAlN046ARkammunG1AA+sRCnTY8hVZyNYIANwkAWAAoUEgoD3wiXABJLEoEGggAY2BPYk4eHT4KKloGJi1dQ15bTn9cZDQoXwCJU358un0jZQlI8qhK6uKMWqg-QIRQ8Oh4JFQ0AHlhgBtcejRiUjzBQpYObkicaLiEiWS0jIm3TF3CInZ2UNHwaCIIGmBt06nZiQWlgmZnlCwAOZyLBwAC2FgkcgsuFwcwgjA4txCaFSc3oCGgKXwzygWNhAFl6GQjG90B8EF9lr9gP8gYMwRCEFCYXCETcQqEAPScqDTADSoTxc0JZF0ACZTDoAMzGLk8-mClki3QAcnoKslKosGrlUBoAAtcHA5jUAO64BAAay0ioJRN0NLgEElonoc2SspC3NISAttuF9p0EtV2s93okCD9ISFyp0ao1uhloSAA) – kaya3 Feb 12 '22 at 01:40
1

Following up on the comment from @matt-diamond

Keep in mind that you're passing in literals here... if the input variables are typed more broadly, you won't have an issue.

If you don't want to have to pass explicitly typed variables each time, you can use this method.

type Num = number | string
 
type AsNum<T> = T extends string ? string : 
   T extends number ? number : never 

declare const max: <T extends Num>(a: T) => (b: AsNum<T>) => AsNum<T>

max(2,4)  max('2','4') // OK
max(2,'4') // Argument of type 'string' is not assignable to parameter of type 'number'
max(true,false) // Argument of type 'boolean' is not assignable to parameter of type 'string | number'

Although it does look just as "clumsy" as your working suggestion, if not more.

reiss
  • 11
  • 2