Typescript 只读对象文字

6
我有一个复杂的数据模型,需要定义40-50个单例“元数据”对象,这些对象满足以下要求:
  • 使用区分联合
  • 除了一些常规字段外,还有一个字典,其中可以包含属于区分联合的任意数量的对象
我希望能够保留为对象文本创建的隐式类型,以便我可以通过智能提示以类型安全的方式操作这些40-50个对象。
以下是使用typescript高级类型文档中的形状的示例:
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;
type Shapes = { [x: string]: Shape };

interface Canvas { 
    name: string;
    size: number;
    //etc
}
interface Drawing extends Canvas {
    shapes: Shapes;
}

//compiles but does not give me intellisense on drawingExplicitType.shapes because it is a Dictionary 
let drawingExplicitType: Drawing = {
    name: 'myDrawing',
    size: 100,
    shapes: {
        c: { kind: "circle", radius: 1 },
        r: { kind: "rectangle", width: 1, height: 1 },
    }
};

//compiles and gives me intellisense on drawingImplicitType.shapes
let drawingImplicitType = {
    name: 'myDrawing',
    size: 100,
    shapes: {
        c: { kind: "circle", radius: 1 },
        r: { kind: "rectangle", width: 1, height: 1 },
    }
};
//will have many objects like drawingImplicitType (around 40-50) which I want to manipulate in a type safe manner
//also the shapes will have sub-shapes, so it's a tree-like data model

//I still want to be able to put all "drawing*" objects in an array of Drawing(s)
//ERROR!!! does not compile: Type 'string' is not assignable to type '"circle"'.
let drawing: Drawing = drawingImplicitType;


//## workaround ##########################################

class Drawing2 implements Canvas {
    name: string;
    size: number;
    shapes: Shapes;

    fromObjLiteral<T extends Pick<Drawing2, Exclude<keyof Drawing2, 'shapes' | 'fromObjLiteral'>> & {shapes: any}>(
        obj: T & {shapes: {readonly [x in keyof T['shapes']]: Shape}}): Drawing2 
    {
        Object.assign(this, obj);
        return this;
    }
}

//compiles and gives me intellisense on drawingImplicitType2.shapes
let drawingImplicitType2 = new (class {
    name = 'myDrawing';
    size = 100;
    shapes = new (class {
        c = new (class { readonly kind = "circle"; radius = 1 })();
        r = new (class { readonly kind = "rectangle"; width = 1; height = 1 })();
    })();
})();

let drawing2: Drawing2 = new Drawing2().fromObjLiteral(drawingImplicitType2);
let array: Drawing2[] = [drawing2];
//The workaround compiles and still allows me to make arrays of Drawing2(s) 
//But I have to create new objects and the syntax is super ugly compared to:
//let drawingImplicitType2 = readonly {...obj literal here...}

编辑 2018-05-25

正如Aleksey L.所指出的,类型断言会更简单易用,但是我们需要在所有子对象上使用它们以获得强类型安全性。这可能会在递归树状数据模型中有点麻烦,其中Shape可以包含其他Shape,但我认为这比new (class {...})()更好。

//type assertions compile but allow me to make all kinds of errors:
let drawingTypeAssertion = {
    name: 'myDrawing',
    size: 100,
    nonExistentField: 123,
    shapes: {
        c: { kind: "circle", radiusWrongField: 1 },
        r: { kind: "rectangle", width: 1, height: 1 },
    }
};
let drawingTA: Drawing = drawingImplicitType as Drawing;

//#####################################################################
// workaround 2: deep type assertions
//#####################################################################

let drawingDeepTypeAssertion = {
    name: 'myDrawing',
    size: 100,
    nonExistentField: 123,
    shapes: {
        c: { kind: "circle", radius: 1 } as Shape,
        r: { kind: "rectangle", width: 1, height: 1 } as Shape,
    }
};
let drawingDTA: Drawing = drawingImplicitType as Drawing;
let array2: Drawing[] = [drawingDTA];
//This workaround compiles and still allows me to make arrays of Drawing2(s)
//no need to create extra objects and the syntax is a bit nicer than new (class {...})() 

2018年5月25日编辑完毕

“解决方法2”的一个很大的缺点是,一旦你使用as Shape,你就失去了隐式类型和在vscode中跳转到定义。

据我所知,问题出在对象字面量是可变的,而辨别联合将不起作用。主要是因为可变性导致辨别字段kind的类型为string,而不是'square''rectangle'

有关深度只读和“const”的讨论很多: https://github.com/Microsoft/TypeScript/issues/10725 https://github.com/Microsoft/TypeScript/issues/15300

在当前版本的typescript中,是否有比上述方法更好的解决方法?

Link to typescript playground.


我认为最简单的解决方法是类型断言: let drawing:Drawing = drawingImplicitType as Drawing; - Aleksey L.
嘿 @AlekseyL.,感谢您的回复,我已更新了游乐场,类型断言允许我编译,但也允许我犯各种各样的错误… 使用 new (class 解决方法时我也会犯 nonExistentField 错误,但是我不会误操作 Shape 的实例。 - acristu
1
@AlekseyL。我可以在每个“Shape”子对象上执行“as Shape”...我会尝试一下,看看这是否比“new (class…”更好。 - acristu
1个回答

1

遇到了类似的问题并找到了另一个解决方案:

从typescript 3.5开始,您还可以使用const assertions,然后:

let drawingTypeAssertion = {
  name: 'myDrawing',
  size: 100,
  nonExistentField: 123,
  shapes: {
     c: { kind: "circle", radiusWrongField: 1 },
     r: { kind: "rectangle", width: 1, height: 1 },
  }
// Add this assertion here
} as const;

这将使得你所有的属性都是只读的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接