Joseph
Joseph

Reputation: 4725

Typescript: Exclude<T, K> doesn't exclude my type

I built a function called compact, what this function do is to remove all falsy values in an array.

This is the javascript implementation of compact:

function compact(arr) {
  return arr.filter(Boolean);
}

const MyData = [0, 1, null, 2, undefined, ''];

console.log(compact(MyData))
// => [1, 2]

This is the Typescript typing part of compact:

type Falsy = false | null | 0 | '' | undefined;

type Compact<T extends any[]> = Exclude<T[number], Falsy>;

// some correct test
type MyData = [0, 1, null, 2, undefined, ''];

type MyDataWithoutFalsy = Compact<MyData>
// => type MyDataWithoutFalsy = 1 | 2

enter image description here

Now, here comes the weird part, when I hook it up with compact code, it's not actually working:

function compact<T extends any[]>(arr: T): Compact<T> {
  return arr.filter(Boolean) as Compact<T>;
}

let MyDataWithoutFalsy = compact([0, 1, null, 2, undefined, '']);
// => let MyDataWithoutFalsy: string | number, but it should be `number` only, because empty string should be excluded.

It should be number only, because empty string should be excluded.

enter image description here

Upvotes: 2

Views: 448

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250106

The problem is not with Exclude, the problem is that for "" (and not just for this string literal, for any string literal) typescript will not usually keep the string literal type but it will rather widen it to string unless we give it a reason to preserve the literal type.

To hint to the compiler you want a literal type the literal must be assigned to a generic type parameter that is constrained to a literal base type:

function compact<V extends (string | undefined | boolean | object | number | null), T extends V[]>(arr: T & V[]): Compact<T> {
    return arr.filter(Boolean) as Compact<T>;
}

let MyDataWithoutFalsy = compact([0, 1, null, 2, undefined, '']); // number


type Falsy = false | null | 0 | '' | undefined;

type Compact<T extends any[]> = Exclude<T[number], Falsy>;

Note that this does mean that compact will not really be usable unless you construct the array in such a way as to preserve literal types (such as '').

function compact<V extends (string | undefined | boolean | object | number | null), T extends V[]>(arr: T & V[]): Compact<T> {
    return arr.filter(Boolean) as Compact<T>;
}
function literalArray<V extends (string | undefined | boolean | object | number | null)>(arr: V[]): V[] {
    return arr.filter(Boolean);
}
let arr = literalArray([0, 1, null, 2, undefined, ''])
let MyDataWithoutFalsy = compact(arr); // 1| 2 ... beacuse resons 

Upvotes: 5

Related Questions