As you've noticed, you don't get string[] | number[] | Date[]
by apply making an array of elements of type string | number | Date
. And since (string | number | Date)[]
is wider than string[] | number[] | Date[]
, you get a compiler error since you can't widen properties of subtypes.
@MattMcCutchen's answer gives some ways to deal with this. Here's another way:
If you have a union like string | number | Date
and want to programmatically turn it into a union of arrays like string[] | number[] | Date[]
, you can use distributive conditional types:
type DistributeArray<T> = T extends any ? T[] : never;
Then you can define TestWithType
in terms of DistributeArray
:
// no error:
interface TestWithType<T extends string | number | Date> extends Test {
values: DistributeArray<T>;
}
And verify that it behaves as you expect:
declare const testWithString: TestWithType<string>
testWithString.values; // string[]
declare const testWithDate: TestWithType<Date>
testWithDate.values; // Date[]
declare const testWithStringOrNumber: TestWithType<string | number>
testWithStringOrNumber.values; // string[] | number[]
Hope that helps. Good luck!
EDIT:
As a related question, is there any way to forbid passing an union type to the generic? (As in requiring to only specify at most one of string, number or date)
Yeah, that's possible, but it requires abusing the type system in a way that makes me uncomfortable. I'd suggest not doing that if you don't need to. Here it is:
type DistributeArray<T> = T extends any ? T[] : never;
type NotAUnion<T> = [T] extends [infer U] ? U extends any ?
T extends U ? unknown : never : never : never
type ErrorMsg = "NO UNIONS ALLOWED, PAL"
interface TestWithType<T extends (
unknown extends NotAUnion<T> ? string | number | Date : ErrorMsg
)> extends Test {
values: DistributeArray<T>
}
declare const testWithString: TestWithType<string> // okay
declare const testWithDate: TestWithType<Date> // okay
declare const testWithStringOrNumber: TestWithType<string | number> // error:
// 'string | number' does not satisfy the constraint '"NO UNIONS ALLOWED, PAL"'.
The type NotAUnion<T>
evaluates to unknown
(a top type) if T
is not a union... otherwise it evaluates to never
(a bottom type). Then, in TestWithType
the type T
is constrained to unknown extends NotAUnion<T> ? string | number | Date : ErrorMsg
, barely skirting the rules against circular references. If you pass a non-union as T
, it becomes T extends string | number | Date
as before. If you pass a union, it becomes T extends ErrorMsg
, a string literal type. Since a union will never extend a string literal, it will fail... And the error message you get will involve ErrorMsg
, so it should hopefully be enough for a developer to realize what's happening. It works, but it's very very sketchy stuff. You asked for it, though.
Good luck again!