Home > Software design >  Can anyone suggest a way to work around this Typescript quirk?
Can anyone suggest a way to work around this Typescript quirk?

Time:01-23

As shown in the playground, I have two functions "noneIs" and "someIs" which check if either none or some item in the given array is of a particular type.

It's important to note that !someIs(arr, type) == noneIs(arr, type) When using !someIs the type is not inferred correctly, can anyone suggest a way to fix this?

Typescript Playground

CodePudding user response:

Will something like this work for you?...

type A = {type: "a", i: number};
type B = {type: "b", someOtherProp: string};
type C = {type: "c", i: number};

type Union = A | B | C;

// returns true if no item in array "arr" is of type "type"
const noneIs = 
    <K extends Union["type"]>
        (arr: Union[], type: K): 
            arr is Exclude<Union, Union & {type: K}>[] => 
                !arr.some(v => v.type == type);

// returns true if some item in array "arr" is of type "type"
const someIs = 
    <K extends Union["type"]>
        (arr: Union[], type: K) => 
            !noneIs(arr, type);


// example array of Union types
const arr: Union[] = [
    {type: "a", i: 1}, 
    {type: "c", i: 2}
];

// this works as expected
if(noneIs(arr, "b")){
    arr[0].i;
}

// Even this doesn't work
// if(!noneIs(arr, "b")){
//    arr[0].i;
//    arr[0].someOtherProp;
// }

// but this does not
if(!someIs(arr, "b")){
    // arr[0].i   // <---- Not working 
    (arr[0] as Exclude<Union, B>).i; // <--- Working, Explicit Type assertion to make it work
}

CodePudding user response:

I'll reiterate my comment on your question and support it with an example:


Even if you inform the compiler that at least one of the array elements matches the type, the compiler still doesn't know which element that is, so the someIs predicate doesn't help to narrow the array. You'll need to narrow individual elements to use them safely (this is related to the same principle which is addressed by the compiler option noUncheckedIndexedAccess).

TS Playground

type A = {type: 'a', i: number};
type B = {type: 'b', someOtherProp: string};
type C = {type: 'c', i: number};

type Union = A | B | C;

// Refactor of your example
function noneAre <T extends Union['type'], A extends readonly Union[]>(
  type: T,
  arr: A,
): arr is { [K in keyof A]: Exclude<A[K], { type: T }> } {
  return arr.every(v => v.type !== type);
}

// Narrow a single `Union` element to a specific type
function isType <
  T extends Union['type'],
  V extends Union,
>(type: T, value: V): value is V & { type: T } {
  return value.type === type;
}


///// Examples:

const a: A = {type: 'a', i: 1};
const b: B = {type: 'b', someOtherProp: 'str'};
const c: C = {type: 'c', i: 1};

const arr = [a, c]; // (A | C)[]

if(noneAre('a', arr)) arr; // arr is C[]
if(noneAre('c', arr)) arr; // arr is A[]

const tuple = [a, c] as [A, C]; // [A, C]

if(noneAre('a', tuple)){
  tuple; // tuple is [never, C]
  tuple[0].i; /*
           ^
Property 'i' does not exist on type 'never'.(2339) */
  tuple[1].i; // number
}

const arr2: Union[] = [b, a, c, c, a /* ... */];

if(isType('b', arr2[0])){
  const [
    first, // element at index 0 is B
    second, // element at index 1 is Union
    third, // element at index 2 is Union
    // ...etc.
  ] = arr2;
}

// Alternatively
const [first] = arr2; // first is Union

if (isType('c', first)) {
  first; // C
}
else if (isType('a', first)) {
  first; // A
}
else {
  first; // B
}

  •  Tags:  
  • Related