Home > Software design >  extends number but do not restrict to a specific number
extends number but do not restrict to a specific number

Time:02-12

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

CodePudding user response:

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.

CodePudding user response:

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.

  • Related