英文:
Why does TypeScript allow assignment of a subtype to an object property?
问题
Consider this example:
type Base = {
a: number,
b: number
}
type SubTest = Base & {
c: number
}
function run(arg: { prop: Base }) {
arg.prop = {
a: 1,
b: 2
};
}
const prop: SubTest = {
a: 1,
b: 2,
c: 3
};
const originalObject = { prop };
run(originalObject);
console.log(originalObject.prop.c.toFixed(2));
TypeScript看起来完全可以处理这个,但生成的JS会产生明显的错误:
originalObject.prop.c is undefined
.tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"strict": true,
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noImplicitThis": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {
"@/": ["./resources/js/"]
},
"types": [
"vite/client"
],
"skipLibCheck": true
},
...
}
英文:
Consider this example:
type Base = {
a: number,
b: number
}
type SubTest = Base & {
c: number
}
function run(arg: { prop: Base }) {
arg.prop = {
a: 1,
b: 2
};
}
const prop: SubTest = {
a: 1,
b: 2,
c: 3
};
const originalObject = { prop };
run(originalObject);
console.log(originalObject.prop.c.toFixed(2));
TypeScript seems perfectly okay with this, but the resulting JS produces the obvious error:
> originalObject.prop.c is undefined
.tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"strict": true,
"sourceMap": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noImplicitThis": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {
"@/*": ["./resources/js/*"]
},
"types": [
"vite/client"
],
"skipLibCheck": true
},
...
}
答案1
得分: 2
Sure, here's the translated code:
由于SubTest
扩展了Base
,这意味着SubTest
是Base
的子类型,因此您可以将任何SubTest
作为Base
传递。您可以将其解释为面向对象编程的工作方式,您可以将子类传递给期望父类的方法。问题在于,对于Typescript而言,一切都没问题,因为您确切地期望一个Base
,如前所述,SubTest
满足这个条件。
之所以可能出现这种赋值情况,是因为Typescript的类型系统被称为结构类型系统,它仅检查类型的所需形状是否已达到,不检查附加字段。简而言之,与Java相比,Typescript仅检查所需字段的存在,而Java则检查确切的标识。
基本上,您可以定义另一种与Base
无关但具有其所有字段的类型,run
不会对其提出异议:
type NotBase = {
a: number;
b: number;
}
const notBase: NotBase = {a: 1, b: 2};
run({ prop: notBase }); // 无错误
如果我们参考官方文档,我们可以看到完全相同的情况发生:
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
const point = { x: 12, y: 26 };
logPoint(point); // 无错误
point
变量从未声明为Point
类型。但是,TypeScript将point
的形状与类型检查中的Point
的形状进行比较。它们具有相同的形状,因此代码通过。形状匹配只需要对象字段的子集匹配。
相同的文档:
const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // 无错误
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // 无错误
为了使函数更安全,您应该添加一个扩展Base
的通用参数:
function run<T extends Base>(arg: { prop: T }) {}
现在,在这个函数中,如果您尝试对arg.prop
进行赋值,您将会得到一个错误:
function run<T extends Base>(arg: { prop: T }) {
arg.prop = { // 这里会出错
a: 1,
b: 2,
};
}
类型' { a: number; b: number; } '不能分配给类型'T'。
'{ a: number; b: number; } '可分配给类型'T'的约束,但'T'可以用不同的约束'Base'的子类型实例化。
这个错误意味着,由于您期望的是扩展Base
的内容,因此不能安全地假设它就是完全的Base
。
要修复此错误,您可以这样做:
function run<T extends Base>(arg: { prop: T }) {
arg.prop = {
...arg.prop,
a: 1,
b: 2,
};
}
这样,您将添加T
的所有必要字段,并更改来自Base
的已知字段(a
和b
)。
作为传播的替代方案,您可以单独分配a
和b
:
arg.prop.a = 1;
arg.prop.b = 1;
或者,您可以使用泛型限制run
再次接受纯粹的Base
:
function run<T extends Base>(arg: { prop: OnlyBase<T> }) {}
现在,让我们定义OnlyBase
:
type OnlyBase<T extends Base> = {} extends Omit<T, keyof Base> ? T : never;
这个类型将检查在删除所有Base
的键后,T
是否会成为空对象。如果是的话,我们可以假设T
是基类,否则返回never
以引发错误。尽管对于编译器而言,这个条件不会改变类型推断,您仍然需要传播arg.prop
以消除先前描述的错误。
测试:
const prop: Base = {
a: 1,
b: 2,
};
const originalObject = { prop };
run(originalObject);
const prop2: SubTest = {
a: 1,
b: 2,
c: 3,
};
const originalObject2 = { prop2 };
run(originalObject2); // 预期错误
[播放区](https://www.typescriptlang
英文:
Since, SubTest
extends Base
, which means that SubTest
is a sub-type of Base
, you can pass any SubTest
as Base
. You can interpret that to how OOP works, you can pass a child class to a method that expects the parent class. The issue here is that for Typescript everything is fine since you expect exactly a Base
and as mentioned before, SubTest
obeys this condition.
The reason why such an assignment is possible is due to Typescript's type system, which is known as structural type system, that only checks if the needed shape of the type is reached and doesn't check for the additional fields. In simple words compared to Java, Typescript checks only for the existence of the required fields, while Java checks for the exact identity.
Basically, you could define another type that is independent of Base
, but has its all fields, and run
wouldn't complain about it:
type NotBase = {
a: number;
b: number;
}
const notBase: NotBase = {a: 1, b: 2};
run({ prop: notBase }); // no error
If we refer to official docs we can see exactly the same situation happening:
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
const point = { x: 12, y: 26 };
logPoint(point); // no error
> The point
variable is never declared to be a Point
type. However, TypeScript compares the shape of point
to the shape of Point
in the type-check. They have the same shape, so the code passes. The shape-matching only requires a subset of the object’s fields to match.
Same docs:
const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // no error
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // no error
To make the function safer, you should add a generic argument that extends Base
:
function run<T extends Base>(arg: { prop: T }) {}
Now, in this function if you try to do the assignment to arg.prop
, you will get an error:
function run<T extends Base>(arg: { prop: T }) {
arg.prop = { // error here
a: 1,
b: 2,
};
}
> Type '{ a: number; b: number; }' is not assignable to type 'T'.
'{ a: number; b: number; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Base'.
This error means that since you expect something that extends Base
it is not safe to assume that it is exactly Base
.
To fix this error you can do the following:
function run<T extends Base>(arg: { prop: T }) {
arg.prop = {
...arg.prop,
a: 1,
b: 2,
};
}
This way you add all necessary fields of T
and change the known ones from the Base
(a
and b
).
As an alternative to spread you could assign a
and b
separately:
arg.prop.a = 1;
arg.prop.b = 1;
Alternatively, you can restrict the run
to accept pure Base
again by using the generics:
function run<T extends Base>(arg: { prop: OnlyBase<T> }) {}
Now, let's define OnlyBase
:
type OnlyBase<T extends Base> = {} extends Omit<T, keyof Base> ? T : never;
This type will check if the T
will be an empty object after removing all of the keys of Base
. If that's true that we can assume that T
is base, otherwise return never
to raise an error. Though, for the compiler this conditional won't change the type inference and you will still have to spread the arg.prop
to remove the error described previously.
Testing:
const prop: Base = {
a: 1,
b: 2,
};
const originalObject = { prop };
run(originalObject);
const prop2: SubTest = {
a: 1,
b: 2,
c: 3,
};
const originalObject2 = { prop2 };
run(originalObject2); // expected error
答案2
得分: 0
TypeScript 允许将子类型分配给对象属性,因为它支持结构类型,并允许对象形状的灵活性。这种行为是有意的,并且是 TypeScript 设计哲学的一部分。
在你的示例中,run 函数接受一个类型为 { prop: Base } 的参数,这意味着它期望一个具有名为 prop 且类型为 Base 的属性的对象。当你将 originalObject 传递给 run 函数时,类型检查器会看到 originalObject.prop 与期望的类型 Base 匹配,因为 SubTest 是 Base 的子类型。这是因为 SubTest 通过添加额外的属性 c 扩展了 Base。
在编译时,TypeScript 允许这种分配,因为它验证提供的对象至少具有 Base 的所需属性。然而,在运行时,当你访问 originalObject.prop.c 时,你会遇到一个错误,因为实际对象没有定义属性 c。
这种行为是静态类型检查和运行时行为之间的一种权衡。TypeScript 的设计目标是在开发过程的早期捕获潜在的类型错误,并通过提供类型安全性和代码补全来提供更好的开发体验。然而,它无法保证运行时行为,因为 JavaScript 是一种动态类型的语言。
为了避免运行时错误,你可以在将其传递给 run 函数时将 originalObject.prop 的类型细化为 SubTest,像这样:
run({ prop: prop });
通过提供具体的类型,类型检查器将在编译时捕获任何不一致之处,并且运行时行为将与期望的类型匹配。
英文:
TypeScript allows assignment of a subtype to an object property because it supports structural typing and allows for flexibility in object shapes. This behavior is intentional and part of TypeScript's design philosophy.
In your example, the run function accepts an argument of type { prop: Base }, which means it expects an object with a property named prop of type Base. When you pass originalObject to the run function, the type checker sees that originalObject.prop matches the expected type Base since SubTest is a subtype of Base. This is because SubTest extends Base by adding an additional property c.
At compile-time, TypeScript allows this assignment because it verifies that the provided object has at least the required properties of Base. However, during runtime, when you access originalObject.prop.c, you will encounter an error because the actual object does not have the c property defined.
This behavior is a trade-off between static type checking and runtime behavior. TypeScript's design aims to catch potential type errors early in the development process and provide a better development experience by providing type safety and code completion. However, it cannot guarantee runtime behavior since JavaScript is a dynamically-typed language.
To avoid runtime errors, you can refine the type of originalObject.prop to SubTest when passing it to the run function, like this:
run({ prop: prop });
By providing the specific type, the type checker will catch any inconsistencies at compile-time, and the runtime behavior will match the expected type.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论