Aidin
Aidin

Reputation: 30047

Typescript: [Compile Time] Writable (- readable) array/object with "as const" type narrowing

Using const assertion, one can nicely narrow an object/array literal's type to its elements.

e.g.

const arr = [
  [5, "hello"],
  [5, "bye"],
] as const;

type T = typeof arr; // type T = readonly [readonly [5, "hello"], readonly [5, "bye"]]

(Without as const, T would be type T = (string | number)[][], which is very wide and sometimes unwanted.)

Now, the problem is that as a result of that as const the array becomes readonly as well, while I just it to have a narrowed type. So, it cannot be passed to the following function.

function fiveLover(pairs: [5, string][]): void {
  pairs.forEach((p) => console.log(p[1]));
}

fiveLover(arr); // Error

And the Error is:

Argument of type 'readonly [readonly [5, "hello"], readonly [5, "bye"]]' is not assignable to parameter of type '[5, string][]'. The type 'readonly [readonly [5, "hello"], readonly [5, "bye"]]' is 'readonly' and cannot be assigned to the mutable type '[5, string][]'.(2345)

Question

How can I narrow the type, without getting the unwanted readonly attribute? (Preferably at the object/array creation time.)

Upvotes: 0

Views: 1132

Answers (2)

Typescript as operator looks a bit hacky for me. I'm trying to avoid it as much as possible. Please, try next example:

const arr = [
  [5, "hello"],
  [5, "bye"],
] as const;

function fiveLover<T extends ReadonlyArray<readonly [5, string]>>(pairs: T): void {
  pairs.forEach((p) => console.log(p[1]));
}

fiveLover(arr); // No Error

Btw, I'm always trying to operate on immutable values. IF you are not mutating your value, there is no sence to remove readonly flag

UPDATE

It is possible to infer without const assertion but you need to provide literal object instead of reference. There is no other way


function fiveLover<
  Fst extends number,
  Scd extends string,
  Tuple extends [Fst, Scd],
  Tuples extends Tuple[]
>(pairs: [...Tuples]): void {
  pairs.forEach((p) => console.log(p[1]));
}

fiveLover([
  [5, "hello"],
  [5, "bye"],
]); // ok

Playground

Upvotes: 1

Aidin
Aidin

Reputation: 30047

In order to get rid of the readonly property and make the object or array mutable, we need the following utility function:

type DeepWritable<T> = { -readonly [P in keyof T]: DeepWritable<T[P]> };

(Source: https://stackoverflow.com/a/43001581)

Now we can do one of the two things:

Solution 1) Make it mutable at the consumption time

That is:

fiveLover(arr as DeepWritable<typeof arr>);

Which is great, and keeps the object intact. But then, if we are using the arr array in several places for similar mutable usage, we need to do it every single time.

Solution 2) Make it mutable at the creation time

Which is what I needed in my real project, since I am using that arr object in a lot of places, and it being readonly causes trouble.

The solution would be defining a utility function, which is just an identity function in run-time, but DeepWritable in compile-time, as following:

function writableIdentity<T>(x: T): DeepWritable<T> {
    return x as DeepWritable<T>;
}

const arr2 = writableIdentity([
  [5, "hello"],
  [5, "bye"],
] as const);

fiveLover(arr2); // Works fine!

TS Playground link.

This way, the object is only manipulated/converted once, and it can be used as an ordinary, type-narrowed, non-readonly variable anywhere!

Upvotes: 0

Related Questions