I have a type Foo
that is an Array
that can contain any number of Bar
elements, with an optional last Qux
element.
Here are some examples of valid data:
[]
[bar]
[qux]
[bar, qux]
[bar, bar, bar, bar, bar]
[bar, bar, bar, qux]
Examples of invalid data:
[qux, qux]
[qux, bar]
[bar, bar, qux, bar]
[bar, bar, qux, qux]
Currently I have it as type Foo = Array<Bar | Qux>
, but this does not capture the fact that only a single Qux
is allowed, and only as the last item.
I'm not sure if I should even expect Typescript to be able to express this, or if there is any practical upside if it could be achieved.
allows leading/middle rest elements in tuple types. You can now write:
type WithLastQux = [...Bar[], Qux]
To allow an optional last Qux
(thanks @last-child):
type WithLastQux = [...Bar[], Qux] | Bar[]
Test:
// ok
const t11: WithLastQux = ["qux"]
const t12: WithLastQux = ["bar", "qux"]
const t13: WithLastQux = ["bar", "bar", "bar", "qux"]
const t14: WithLastQux = ["bar"]
const t15: WithLastQux = []
// error
const t16: WithLastQux = ["qux", "qux"]
const t17: WithLastQux = ["qux", "bar"]
As extension, define a generic function helper to keep narrow, fixed-sized tuple types:
function foo<T extends WithLastQux>(t: T): T { return t }
foo(["bar", "qux"])
// type: function foo<["bar", "qux"]>(t: ["bar", "qux"]): ["bar", "qux"]
Note, that an optional last Qux
element via [...Bar[], Qux?]
is not directly possible:
The only restriction is that a rest element can be placed anywhere in a tuple, so long as it’s not followed by another optional element or rest element. (docs)
Playground
In JS, rest parameters always need to be the last function parameter. TS 4.0 comes with a feature called variadic tuple types, which you can think of here as "rest parameters for types".
The neat thing is: variadic tuple types can be placed anywhere in function parameters, not just at the end. We now can define an arbitrary amount of bar
items with an optional last element qux
.
Example: [bar, bar, <and arbitrary more>, qux]
can be interpreted as variadic tuple [...T, qux]
, where T
stands for all the bar
items.
Assert
typetype Assert<T extends readonly (Bar | Qux)[]> =
T extends readonly [] ? T :
T extends readonly [...infer I, infer U] ?
I extends [] ? T :
I extends Bar[] ? T : readonly [...{ [K in keyof I]: Bar }, U] :
never
function assert<T extends readonly (Bar | Qux)[] | readonly [Bar | Qux]>(
a: Assert<T>): T { return a as T }
// OK
const ok1 = assert([])
const ok2 = assert(["bar"])
const ok3 = assert(["qux"])
const ok4 = assert(["bar", "qux"])
const ok5 = assert(["bar", "bar", "bar"])
const ok6 = assert(["bar", "bar", "bar", "qux"])
// errors
const err1 = assert(["qux", "qux"])
const err2 = assert(["qux", "bar"])
const err3 = assert(["bar", "bar", "qux", "bar"])
const err4 = assert(["bar", "bar", "qux", "qux"])
You might substitute the assert
helper function with some kind of "sentinel" type:
type Sentinel<T, U extends T> = U
const arr1 = ["bar", "bar"] as const
const arr2 = ["qux", "bar"] as const
type Arr1Type = Sentinel<Assert<typeof arr1>, typeof arr1> // OK
type Arr2Type = Sentinel<Assert<typeof arr2>, typeof arr2> // error
Playground
type Bar = "bar"
type Qux = "qux"
function assert<T extends Bar[] = []>(
arg: (Qux | Bar)[] extends [...T, Qux] ? never : [...T, Qux]): [...T, Qux]
function assert<T extends Bar[] = []>(arg: [...T]): [...T]
function assert<T extends Bar[] = []>(arg: [...T, Qux?]) { return arg }
Playground
If you really need rest parameters followed by a last element (the specification only allows them to be last, see @Nit's answer), here is an alternative:
as const
)StringToNumber
hardcoded size limit)Bar
/Qux
asserting type in a recursive manner: while technically possible, recursive types are not officially supported till version 4.1 (Note: updated).type Bar = { bar: string };
type Qux = { qux: number };
// asserts Qux for the last index of T, other elements have to be Bar
type AssertQuxIsLast<T extends readonly any[]> = {
[K in keyof T]: StringToNumber[Extract<K, string>] extends LastIndex<T>
? T[K] extends Qux
? Qux
: never
: T[K] extends Bar
? Bar
: never;
};
// = calculates T array length minus 1
type LastIndex<T extends readonly any[]> = ((...t: T) => void) extends ((
x: any,
...u: infer U
) => void)
? U["length"]
: never;
// TS doesn't support string to number type conversions 😥,
// so we support (index) numbers up to 256
export type StringToNumber = {
[k: string]: number;
0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99, 100: 100, 101: 101, 102: 102, 103: 103, 104: 104, 105: 105, 106: 106, 107: 107, 108: 108, 109: 109, 110: 110, 111: 111, 112: 112, 113: 113, 114: 114, 115: 115, 116: 116, 117: 117, 118: 118, 119: 119, 120: 120, 121: 121, 122: 122, 123: 123, 124: 124, 125: 125, 126: 126, 127: 127, 128: 128, 129: 129, 130: 130, 131: 131, 132: 132, 133: 133, 134: 134, 135: 135, 136: 136, 137: 137, 138: 138, 139: 139, 140: 140, 141: 141, 142: 142, 143: 143, 144: 144, 145: 145, 146: 146, 147: 147, 148: 148, 149: 149, 150: 150, 151: 151, 152: 152, 153: 153, 154: 154, 155: 155, 156: 156, 157: 157, 158: 158, 159: 159, 160: 160, 161: 161, 162: 162, 163: 163, 164: 164, 165: 165, 166: 166, 167: 167, 168: 168, 169: 169, 170: 170, 171: 171, 172: 172, 173: 173, 174: 174, 175: 175, 176: 176, 177: 177, 178: 178, 179: 179, 180: 180, 181: 181, 182: 182, 183: 183, 184: 184, 185: 185, 186: 186, 187: 187, 188: 188, 189: 189, 190: 190, 191: 191, 192: 192, 193: 193, 194: 194, 195: 195, 196: 196, 197: 197, 198: 198, 199: 199, 200: 200, 201: 201, 202: 202, 203: 203, 204: 204, 205: 205, 206: 206, 207: 207, 208: 208, 209: 209, 210: 210, 211: 211, 212: 212, 213: 213, 214: 214, 215: 215, 216: 216, 217: 217, 218: 218, 219: 219, 220: 220, 221: 221, 222: 222, 223: 223, 224: 224, 225: 225, 226: 226, 227: 227, 228: 228, 229: 229, 230: 230, 231: 231, 232: 232, 233: 233, 234: 234, 235: 235, 236: 236, 237: 237, 238: 238, 239: 239, 240: 240, 241: 241, 242: 242, 243: 243, 244: 244, 245: 245, 246: 246, 247: 247, 248: 248, 249: 249, 250: 250, 251: 251, 252: 252, 253: 253, 254: 254, 255: 255
};
const arr1 = [{ bar: "bar1" }, { bar: "foo1" }, { qux: 42 }] as const
const arr2 = [{ bar: "bar2" }, { bar: "foo2" }] as const
const arr3 = [{ qux: 42 }] as const
const typedArr1: AssertQuxIsLast<typeof arr1> = arr1
const typedArr2: AssertQuxIsLast<typeof arr2> = arr2 // error (OK)
const typedArr3: AssertQuxIsLast<typeof arr3> = arr3
AssertQuxIsLast
is a mapped tuple type. Each key K
in T
is an index of the form "0"
,"1"
,"2"
, etc. . But this type is a string
, not a number
!
In order to assert Qux
for the last index, we need to convert K
back to number
in order to compare it with T['length']
, which returns the array length number
. As the compiler doesn't support a dynamic string to number conversion yet, we have created our own converter with StringToNumber
.
Playground
Since in Javascript the rest parameters are only allowed as the last parameter, the same approach was historically adopted for types in Typescript.
There are longstanding tickets to remove this limitation for types, see here and here, however at the moment, it's still not supported.
Given you can't strictly define specific-followed-by-rest tuples in Typescript, there doesn't seem to be a straightforward way to implement this until the functionality is added to Typescript itself.
Alternatively, you can manually generate a comfortable number of valid type lists in a separate build step, but at that point I doubt the gained value is worth it.
This functionality is expected to ship with TS 4.2.
Example from the implementing PR:
function f1(...args: [...string[], number]) {
const strs = args.slice(0, -1) as string[];
const num = args[args.length - 1] as number;
// ...
}
f1(5);
f1('abc', 5);
f1('abc', 'def', 5);
f1('abc', 'def', 5, 6); // Error
Your type will become
type Foo = [...Bar[], Qux]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With