我想要 TS 中完全不可变的对象



>我有一些大对象,比如

const a={
b:33, 
c:[78, 99], 
d:{e:{f:{g:true, h:{boom:'selecta'}}}};/// well, even deeper than this...

我希望 TS不允许我这样做

a.d.e.f.h.boom='respek';

如何完全改变对象?是否仅通过为每个深度嵌套对象创建具有"只读"和接口的接口?

我们现在有as const选项,这是@phil294提到的第一个选项(嵌套readonly)的语法简洁方式。

const a = {
b: 33,
c: [78, 99],
d:{e:{f:{g:true, h:{boom:'selecta'}}}}
} as const;
a.d.e.f.h.boom = 'respek'; //Cannot assign to 'boom' because it is a read-only property.ts(2540)

作为额外的好处,您可以使用以下技巧使嵌套函数的输入不可变:

type ImmutableObject<T> = {
readonly [K in keyof T]: Immutable<T[K]>;
}
export type Immutable<T> = {
readonly [K in keyof T]: T[K] extends Function ? T[K] : ImmutableObject<T[K]>;
}

所以这会发生

const a = {
b: 33,
c: [78, 99],
d:{e:{f:{g:true, h:{boom:'selecta'}}}}
}
function mutateImmutable(input: Immutable<typeof a>) {
input.d.e.f.h.boom = 'respek'; //Cannot assign to 'boom' because it is a read-only property.ts(2540)
}

如 https://www.typescriptlang.org/docs/handbook/interfaces.html 中所述,您可以在类/接口属性上使用readonly,或者对不可变对象和数组使用Readonly<...>/ReadonlyArray<>。在您的情况下,这将如下所示:

const a: Readonly<{
b: number,
c: ReadonlyArray<number>,
d: Readonly<{
e: Readonly<{
f: Readonly<{
g: boolean,
h: Readonly<{
boom: string
}>
}>
}>
}>
}> = {
b: 33,
c: [78, 99],
d:{e:{f:{g:true, h:{boom:'selecta'}}}}
}
a.d.e.f.h.boom = 'respek'; // error: Cannot assign to 'boom' because it is a constant or a read-only property.

显然,这是非常同义重复的陈述,所以我建议你为你的对象定义适当的类结构。你并没有通过仅仅声明一个嵌套的、非类型化的对象来真正利用 Typescript 的任何功能。

但是如果你真的需要没有类型定义,我认为唯一的方法是像 Hampus 建议的那样定义一个冰箱(喜欢术语 :D)。取自 MDNdeepFreeze(obj)函数:

function freezer(obj) {
Object.getOwnPropertyNames(obj).forEach(name => {
if (typeof obj[name] == 'object' && obj[name] !== null)
freezer(obj[name]);
});
return Object.freeze(obj);
}
const a = freezer({
b:33, 
c:[78, 99], 
d:{e:{f:{g:true, h:{boom:'selecta'}}}}});
a.d.e.f.h.boom='respek'; // this does NOT throw an error. it simply does not override the value.

tl;dr:如果不定义类型,就无法获得编译器类型错误。这就是打字稿的重点。

编辑:

最后这句话是错误的。例如

let a = 1
a = "hello"

将引发错误,因为类型隐式设置为 number。但是,对于只读,我认为您将需要如上所述的正确声明。

Minko Gechev 创建了 DeepReadonly 类型:

type DeepReadonly<T> =
T extends (infer R)[] ? DeepReadonlyArray<R> :
T extends Function ? T :
T extends object ? DeepReadonlyObject<T> :
T;
interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
type DeepReadonlyObject<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
};
interface Person {
name: string;
job: { company: string, position:string };
}
const person: DeepReadonly<Person> = {
name: 'Minko',
job: {
company: 'Google',
position: 'Software engineer'
}
};
person.job.company = 'Alphabet'; // Error

看看 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze 或 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

Object.freeze可能会做你想做的事,但至少WebStorm在尝试编辑对象时不会警告你,Chrome会静默失败。

const obj = { val: 1 }; 
Object.freeze(obj);
obj.val = 2;
console.log(obj);
-> { val: 1 }

不能向冻结对象的属性集添加或删除任何内容。任何这样做的尝试都将失败,无论是静默还是通过引发 TypeError 异常失败

这有效:

const a= new class {
readonly b = 33, 
readonly c:ReadonlyArray<number> = [78, 99], 
readonly d = new class {
readonly e = new class {
readonly f = new class {
readonly g:true, 
readonly h: new class {
readonly boom:'selecta'}}}};

其他OOP语言默认具有类不变性,TypeScript也不例外。你可以使用经典的OO方式,比如在Java或PHP中:

export class ClassicImmutableObject {
private readonly _stringProperty: string;
private readonly _numberProperty: number;
constructor(stringProperty: string, numberProperty: number) {
this._stringProperty = stringProperty;
this._numberProperty = numberProperty;
}

get stringProperty(): string {
return this._stringProperty;
}
get numberProperty(): number {
return this._numberProperty;
}
withStringProperty(stringProperty: string): ClassicImmutableObject {
return new ClassicImmutableObject(stringProperty, this._numberProperty);
}
withNumberProperty(numberProperty: number): ClassicImmutableObject {
return new ClassicImmutableObject(this._stringProperty, numberProperty);
}
}

然后,您可以在代码中使用它:

import { ClassicImmutableObject } from './classic-immutable-object';
//...
immutableObjectExperiment(): string {
const immutableObject: ClassicImmutableObject = 
new ClassicImmutableObject('test string', 123);
// this is not allowed: (TS2540 Can not assign to read-only property)
//immutableObject.stringProperty = 'modified';
let result = `Original classic immutable object: ${JSON.stringify(immutableObject)} rn`;
result += 'This demonstrates how to use getters:rn';
result += `stringProperty value: ${immutableObject.stringProperty}rn`;
result += `numberProperty value: ${immutableObject.numberProperty}rnrn`;
const modifiedImmutableObject: ClassicImmutableObject =
immutableObject.withStringProperty('modified test string');
result += `Modified classic immutable object with only stringProperty changed: ${JSON.stringify(modifiedImmutableObject)}rn`;
result += `Original immutable object is still available and unchanged: ${JSON.stringify(immutableObject)}`;
return result;
}

此函数的结果应为:

Original classic immutable object: {"_stringProperty":"test string","_numberProperty":123}
This demonstrates how to use getters:
stringProperty value: test string
numberProperty value: 123
Modified classic immutable object with only stringProperty changed: {"_stringProperty":"modified test string","_numberProperty":123}
Original immutable object is still available and unchanged: {"_stringProperty":"test string","_numberProperty":123}

最新更新