4

The following example has a ts error at test

type Test = {
    obj: object;
    arr: string[];
};

export const test: Test = {
    obj: {},
    arr: ['foo'],
} as const;

saying:

Type '{ readonly obj: {}; readonly arr: readonly ["foo"]; }' is not assignable to type 'Test'. Types of property 'arr' are incompatible. The type 'readonly ["foo"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

I want to use as const (causing the error) with test cuz test should never be changed. At the same time I will have other mutable variables of type Test, so I can't have any of the fields be readonly. How do I make the error go away with my goal in mind?

Confusingly, obj causes no error. Arrays are technically objects.

sdfsdf
  • 5,052
  • 9
  • 42
  • 75
  • 1
    Explicit type with `as const` assertion does not supported. You should use one or another. – captain-yossarian from Ukraine Jun 30 '22 at 05:47
  • Does this answer your question? [Inconsistency with Typescript Readonly type](https://stackoverflow.com/questions/62225769/inconsistency-with-typescript-readonly-type) – Vega Jun 30 '22 at 05:53
  • There are many more – Vega Jun 30 '22 at 05:54
  • The problem as I understand it is that `as const` narrows the type of the `arr` to be a tuple (array of fixed size) with elements of the string literal type - in this case `['foo']`. Since `['foo']` is more specific than `string[]`, it is not allowed. Is that correct? – GregL Jun 30 '22 at 12:17

4 Answers4

1

This seems to work:

type Test = {
    obj: object;
    arr: string[];
};

export const test: Test = {
    obj: {},
    arr: ['foo'] as string[]
} as const;
smac89
  • 39,374
  • 15
  • 132
  • 179
1

Could you make use of the Readonly utility type?

Adapted Answer

Playground Link

@GregL pointed out that this would not stop the array being mutated with push, so I propose a DeepReadonly, which will recursively apply Readonly to all object based children.

type Test = {
    obj: object;
    arr: string[];
};

type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : Readonly<T[P]>
}

const test: DeepReadonly<Test> = {
    obj: {},
    arr: ['foo'],
}

test.obj = {}

test.arr = []

test.arr.push('')

Previous Answer

Readonly Documentation

Playground Link

type Test = {
    obj: object;
    arr: string[];
};

const test: Readonly<Test> = {
    obj: {},
    arr: ['foo'],
}

// Error
test.obj = {}

// Error
test.arr = []

This leaves your original type untouched but does not allow your variant to be changed.

N.J.Dawson
  • 1,220
  • 10
  • 28
  • 1
    The downside of this approach is that `test.arr.push('bar')` is NOT an error when I think the OP wants it to be. – GregL Jun 30 '22 at 12:11
  • @GregL I've adapted my answer for your comment, thanks. Can you see any issue in the adapted answer? – N.J.Dawson Jun 30 '22 at 13:11
0

You would have to add readonly to the type of the field arr in Test.

type Test = {
    str: string;
    arr: readonly string[];
};

Edit: given your preference to keep the Test arr field mutable, you could try something like this. Make arr a union type of both string[] and readonly string[].

However, you will immediately run into other issues, because if TypeScript doesn't know which type arr has for a given instance of Test, it will assume the most restrictive behavior and enforce that you can't actually mutate the array.

The solution is to have utility functions like pushIfNotReadonly() below which can use type guards to ensure the given array is in fact mutable before trying to do such an operation. But the "failure" behavior is kind of undefined - maybe you just don't push the value into the array, but how do you account for that tricky behavior in your logic? You'll quickly lose track of what is happening, useful type checking goes out the window and you get yourself into a mess.

What you're trying to do just isn't very compatible with TypeScript patterns and should be avoided if you can. I recommend you rethink how you are using type definitions and what your fundamental intent is, and evaluate other options.

type Test = {
    obj: object;
    arr: string[] | readonly string[];
};

export const test1: Test = {
    obj: {},
    arr: ['foo'],
} as const;

export const test2: Test = {
    obj: {},
    arr: ['bar'],
}

function pushIfNotReadonly(arr: Test['arr'], value: string) {
    if (Array.isArray(arr)) {
        arr.push(value)
    }
}

pushIfNotReadonly(test1.arr, 'string'); // will not push because it is readonly
pushIfNotReadonly(test2.arr, 'string'); // will push because it is NOT readonly
jered
  • 11,220
  • 2
  • 23
  • 34
  • I don't want that field to be readonly though. I'll add that to my question – sdfsdf Jun 30 '22 at 05:27
  • Thanks. I think you're right that this isn't compatible with TypeScript patterns. I think it's best that I remove the `as const` and hope the object is never mutated. – sdfsdf Jun 30 '22 at 05:52
0
type Test = {
  str: string;
  arr: string[];
};

export const test: Test = {
  str: 'Hello',
  arr: ['foo'] as string[],
} as const;

If you used as const with Test then you need to define type inside read-only and for more details refer to this document TypeScript Read Only