Issue
With noUncheckedUndexedAccess
, being explicit with array indexes is necessary:
const someArray: number[] = [];
const value = someArray[0]!; // i have to say "i know what i am doing"
However, for destructuring, i couldn't find a simple way to do so:
const execResult = /a(.)(.)/.exec('axy');
if (execResult === null) throw new Error('Regex should match');
const [, first, second] = execResult; // each get `string | undefined`
Is there any similar short way to remove undefined
from the types here? I can type-cast the right side, but that is both unsafe, and i need to write out the entire type again. In this case, it's completely clear, that the elements exist, as the regex has two capturing groups.
Solution
One possibility would be a generic helper-function, which does the cast. The first versions were shorter (see edit history), but while the preamble here is long, it can just be tucked away in some utility package.
type ExpandTypeTooltip<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type Equals<T, U, OnEqual, OnNotEqual> =
(<X>() => X extends T ? 0 : 1) extends
(<X>() => X extends U ? 0 : 1)
? OnEqual
: OnNotEqual;
type HasReadonlyProperties<T, OnHasReadonly, OnHasNoReadonly> =
Equals<T, { -readonly [K in keyof T]: T[K] }, OnHasNoReadonly, OnHasReadonly>;
type IntegersTo<T extends number, Counter extends readonly any[] = []> = Counter['length'] extends T
? Counter[number]
: IntegersTo<T, [...Counter, Counter['length']]>;
type MAX_ASSERTABLE_ELEMENTS = 64;
type PropagateReadonlyToNonNullHeadTuple<Input extends readonly any[], Output extends readonly any[]> =
// 'length' is the easiest to check, but tuples in TS 4.5.4 don't have readonly 'length' (bug?)
// Therefore, check named indices too (indices with an additional string-key property, TS differentiates number and string keys)
HasReadonlyProperties<Pick<Input, Extract<keyof Input, 'length' | `${number}`>>, Readonly<Output>, Output>;
type NonNullHeadTupleIterator<
Tail extends readonly any[],
MaxIterations extends number,
Head extends readonly any[],
> = Head['length'] extends MaxIterations // Also check if T's length was reached
? [...Head, ...Tail] // Return constructed tuple
: Tail extends readonly [infer MovingElement, ...infer RemainingTail] // Split off one element from T
? NonNullHeadTupleIterator<RemainingTail, MaxIterations, [...Head, NonNullable<MovingElement>]> // Add that element to head
: NonNullHeadTupleIterator<Tail, MaxIterations, [...Head, NonNullable<Tail[Head['length']]>]> // if split fails, Tail is an array
type NonNullHeadTuple<
T extends readonly any[],
N extends number,
> = PropagateReadonlyToNonNullHeadTuple<
T,
NonNullHeadTupleIterator<
Required<T>, // [infer Head, ...infer Tail] breaks on optional elements
number extends T['length'] ? N : N | T['length'], // Also check if T's length was reached, if finite
[]
>
>;
type HomomorphismValueNever<T> = { [K in keyof T]: never };
type NonNullHeadStructure<
T extends readonly any[],
N extends number,
> = T extends any // distribute over unions
? HomomorphismValueNever<T>['length'] extends never // is T a pure array structure
? NonNullHeadTuple<T, N> & ExpandTypeTooltip<Omit<T, keyof any[] | `${number}`>> // impure, intersect potential extras
: NonNullHeadTuple<T, N> // pure, don't touch
: never;
const assertNonNullHead = <T extends readonly any[], N extends IntegersTo<MAX_ASSERTABLE_ELEMENTS> = 6>
(x: T, n: N = 6 as any) => x as NonNullHeadStructure<T, N>;
// Origin use case
const execResult = /a(.)(.)/.exec('axy');
if (execResult === null) throw new Error('Regex should match');
const [, first, second, nonAsserted] = assertNonNullHead(execResult, 3);
/* Some other tests. Things to look out for:
* - Keeps original length, even if `T['length'] < N`
* - generates tuples with set types for the first N elements, and identical types for tail
* - removes `undefined | null` from first N elements
* - propagates `readonly` from input (unless someone deliberately generates abominations of types that bypass detection)
* - works on arrays, finite tuples, open-ended tuples (optional elements, rest elements),
* and their intersections with other objects
* - keeps true tuple/array structure in tact (doesn't intersect if not needed), even regenerates some
*
* Note: I don't know any way to keep true tuple structure in tact, yet not use `Required` along the way.
* This leads to a very minor inconveniency, that all optional tuple elements past N are also set to required.
*/
const t1 = [1, 'str', { a: 0 }] as const;
declare const t2: readonly [1 | null, 'str' | undefined, { a?: 0 }?];
declare const t3: (string | number)[];
declare const t4: [number, number, ...string[]];
declare const t5: (string | null)[];
declare const t6: (string | null)[] & { readonly extra?: 0 };
declare const t7: [number, number, number, number, number, number, string?];
assertNonNullHead(t1); // readonly [1, "str", { readonly a: 0 }]
assertNonNullHead(t2); // readonly [1, "str", { a?: 0 | undefined }]
assertNonNullHead(t3); // [string | number, string | number, string | number, string | number,
// string | number, string | number, ...(string | number)[]]
assertNonNullHead(t4); // [number, number, string, string, string, string, ...string[]]
assertNonNullHead(t5); // [string, string, string, string, string, string, ...(string | null)[]]
assertNonNullHead(t6); // [string, string, string, string, string, string, ...(string | null)[]]
// & { readonly extra?: 0 | undefined }
assertNonNullHead(t7); // Sadly sets optional elements past N to required as well
This is slightly more verbose than a non-null-assertion-operator !
, but not by much. While hard-coding limits is usually bad practice, i cannot think of a case, where destructuring more than 32 elements in one statement is ever a good idea to begin with.
Note, that identity functions are, for hot code, typically completely erased by modern engines' function inliners. Therefore, performance differences compared to an even more verbose cast (execResult as typeof execResult & NonNullTupleFromArrayHead<typeof execResult>
) are not really a concern.
Better solutions, and optimizations for this one, are welcome.
Note, that a minor issue is not being able to keep later optional tuple elements, while also not deforming true array structures. This should almost never be a real world problem though, and maybe someone finds a way, or things change on future TS versions.
Answered By - Doofus
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.