Mohammad Reza Ghasemi
Mohammad Reza Ghasemi

Reputation: 363

How to create a generic union type related to the other one?

I want to create a union type consist of numbers related to the other prop value.

type Props = { numberOfPoints: number; activeItem: number }

If numberOfPoints = 3 so activeItem be a union type consist of 1 | 2 | 3 according to numberOfPoints value.

Upvotes: 1

Views: 110

Answers (1)

Requirements: You need to use TypeScript nightly

Similar question/answer you can find here

The code:



type MAXIMUM_ALLOWED_BOUNDARY = 999

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, [...Result, 0]['length']]>
    )

type Props<PointsCount extends number> = {
    numberOfPoints: PointsCount; activeItem: ComputeRange<PointsCount>
}

type Destructure<T extends Props<any>> =
    T extends { numberOfPoints: infer Count; activeItem: infer Union }
    ? Union extends number[]
    ? { numberOfPoints: Count; activeItem: Union[number] }
    : never
    : never

type Result = Destructure<Props<3>>

Playground

Explanation

This PR increases a limit of recursive types.

ComputeRange - creates empty array on first call and checks whether the length of an array equals N generic argument. If not, calls recursively itself with same first argument and extended version of second argument (with extra element). This approach allows us to keep track of array length.

Each element of array is the length of Result during each iteration.

Destructure helps you to get the union of all elements in array. You are unable to do ComputeRange<PointsCount>[number] in Props directly because of the recursion.

Caveats (thanks @jcalz for pointing it)


You still are allowed to use negative numbers, typeof Infinity and fractionals.

// activeItem: any;
type _ = Props<2.3>

// activeItem: []
type __ = Props<number>

// activeItem: any;
type ___ = Props<0.000001>

// activeItem: [];
type ____ = Props<typeof Infinity>

The better way is to restrict Props argument:

type AllowedRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]

type Props<PointsCount extends AllowedRange> = {
    numberOfPoints: PointsCount; activeItem: ComputeRange<PointsCount>
}

See full code:

type MAXIMUM_ALLOWED_BOUNDARY = 999

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, [...Result, 0]['length']]>
    )

type AllowedRange = ComputeRange<MAXIMUM_ALLOWED_BOUNDARY>[number]

type Props<PointsCount extends AllowedRange> = {
    numberOfPoints: PointsCount; activeItem: ComputeRange<PointsCount>
}

type Destructure<T extends Props<any>> =
    T extends { numberOfPoints: infer Count; activeItem: infer Union }
    ? Union extends number[]
    ? { numberOfPoints: Count; activeItem: Union[number] }
    : never
    : never
/**
 * Ok
 */

type Ok0 = Destructure<Props<42>> // ok
type Ok1 = Destructure<Props<600>> // ok

/**
 * Fails
 */
// activeItem: any;
type _ = Props<2.3>

// activeItem: []
type __ = Props<number>

// activeItem: any;
type ___ = Props<0.000001>

// activeItem: [];
type ____ = Props<typeof Infinity>

Please keep in mind, these things with recursion could boil a glass of water on your CPU :D

Playground


If you don't want to pay a huge amount of money for electricity, you can add some validators for number argument:

type ComputeRange<
    N extends number,
    Result extends Array<unknown> = [],
    > =
    (Result['length'] extends N
        ? Result
        : ComputeRange<N, [...Result, [...Result, 0]['length']]>
    )

type IsFraction<T extends number> = `${T}` extends `${number}.${number}` ? true : false
type IsNegative<T extends number> = `${T}` extends `-${number}` ? true : false
type IsNotLiteral<T> = T extends number ? number extends T ? true : false : false

type Either<Validators extends boolean[]> = Validators[number] extends false ? true : false

type IsValid<PointsCount extends number> = Either<[
    IsFraction<PointsCount>,
    IsNegative<PointsCount>,
    IsNotLiteral<PointsCount>
]
>
{
    type Test = IsValid<2.3> // false
    type Test1 = IsValid<-1> // false
    type Test2 = IsValid<number> // false

}

type Props<
    PointsCount extends number> =
    IsValid<PointsCount> extends false ? never : {
        numberOfPoints: PointsCount; activeItem: ComputeRange<PointsCount>
    }

type Destructure<T> =
    T extends { numberOfPoints: infer Count; activeItem: infer Union }
    ? Union extends number[]
    ? { numberOfPoints: Count; activeItem: Union[number] }
    : never
    : never
/**
 * Returns union
 */

type Ok0 = Destructure<Props<2>> // expected result

/**
 * Never
 */
type _ = Props<2.3>
type __ = Props<number> 
type ___ = Props<0.000001>
type ____ = Props<typeof Infinity>

Playground

IsFraction, IsNegative, IsNotLiteral will check whether your argument is valid or not. Please consider adding more tests.

P.S. More explanation you can find in my article

Upvotes: 2

Related Questions