处理 TypeScript 中类型改变的副作用
Dealing with type changing side effects in TypeScript
这更像是一个关于如何处理 TypeScript
中具有 类型改变副作用 的函数的开放性问题。我知道并且非常同意这个概念,即函数应该尽可能少的副作用,如果有的话。
但有时,希望 就地 更改对象(及其类型),而不是使用另一种静态类型创建它的新副本。我最常遇到的原因是可读性、效率或减少行数.
因为我原来的例子太复杂了,这里有一个(希望如此)非常基本的例子:
type KeyList = 'list' | 'of' | 'some' | 'keys';
// Original type (e.g. loaded from a JSON file)
interface Mappable {
source: { [K in KeyList]: SomeNestedObject },
sourceOrder: KeyList[];
}
// Mapped Type (mapped for easier access)
interface Mapped {
source: { [K in KeyList]: SomeNestedObject },
sourceOrder: SomeDeepObject[];
}
// What I have to do to keep suggestions and strict types all the way
const json: Mappable = JSON.parse(data); // ignoring validation for now
const mapped: Mapped = toMappedData(json);
// What I would like to to
const mapped: Mappable = JSON.parse(data);
mapData(mapped); // mapped is now of type Mapped
原因,为什么我想改变对象属性和它的类型可能是:
json
对象非常大,在内存中保存 2 个副本会适得其反
- 创建
json
对象的深拷贝到mapped
对象中非常麻烦
我不认为“我想做什么”下的代码可读性很好,不管它是否工作。 我正在寻找一种干净且类型安全的方法来解决此问题。或者,或者,建议或想法来扩展 typescript 的功能以解决此问题。
非常感谢对此的任何建议、想法和评论!也许我对此太深入了,看不到真正简单的解决方案。
我目前的做法是:
type KeyList = 'list' | 'of' | 'some' | 'keys';
// Merged the types to keep the code a little shorter
// Also makes clear, that I don't alter the type's structure
interface MyType<M ext boolean = true> {
source: { [K in KeyList]: SomeNestedObject },
sourceOrder: (M ? SomeNestedObject : (KeyList | SomeNestedObject))[];
}
function mapData(data: MyType<true>): MyType {
const { source, sourceOrder } = data.sourceOrder
for (let i = 0; i < sourceOrder.length; i++) {
if (typeof sourceOrder[i] == 'string') {
sourceOrder[i] = source[sourceOrder[i]];
}
}
return (data as unknown) as MyType;
}
const json: MyType<true> = JSON.parse(data);
const mapped: MyType = mapData(json);
// mapped now references json instead of being a clone
我不喜欢这种方法的地方:
- 它不是类型安全的。打字稿无法检查我是否正确地改变了类型
- 因此我必须转换为未知,这并不理想
json
类型不那么严格。可能会对代码建议产生负面影响
- 该函数有一个
side effect
和一个 return type
(只有 side effect
或 return type
会更干净)
我不认为你想做的是可能的。您要求打字稿更改变量类型作为副作用。但这会带来各种并发症。
如果 mapData
函数是 运行 有条件的怎么办?
const mapped: Mappable = JSON.parse(data);
if (Math.random() > 0.5) {
mapData(mapped); // mapped is now of type Mapped
}
// What type is `mapped` here?
或者,如果您在转换之前传递对此对象的引用会怎样?
function doAsyncStuff(obj: Mappable) {}
doAsyncStuff(mapped)
mapData(mapped)
// Then later, doAsyncStuff(obj) runs but `obj` is a different type than expected
我认为你能在这里得到的最接近的是类型保护,它转换为中间类型,支持预转换类型和 post-transform 类型的联合,你可以在其中实际进行转换.
interface A {
foo: string
}
interface B {
foo: string
bar: string
}
interface A2B {
foo: string
bar?: string
}
function transform(obj: A): obj is B {
const transitionObj: A2B = obj
transitionObj.bar = "abc" // Mutate obj in place from type A to type B
return true
}
const obj: A = { foo: 'foo' }
if (transform(obj)) {
obj // type is B in this scope
}
obj // but obj is still type A here, which could lead to bugs.
但是,如果您实际上在该条件之外使用 obj
作为类型 A
,那么这可能会导致 运行 时间错误,因为类型是错误的。所以带有副作用的类型保护函数也是一个非常糟糕的主意,因为类型保护可以让你覆盖类型脚本的正常输入。
我真的认为您已经在这里做了最好的方法,尤其是 如果输出类型与输入类型不同。将新对象不可变地构造为新类型。
const mapped: Mapped = toMappedData(json);
如果性能或内存是一个大问题,那么您可能不得不为此牺牲类型安全性。编写健壮的单元测试,将其投射到任何单元测试,添加关于那里发生的事情的非常突出的评论。但是除非您一次处理数百 MB 的数据,否则我敢打赌这真的没有必要。
这更像是一个关于如何处理 TypeScript
中具有 类型改变副作用 的函数的开放性问题。我知道并且非常同意这个概念,即函数应该尽可能少的副作用,如果有的话。
但有时,希望 就地 更改对象(及其类型),而不是使用另一种静态类型创建它的新副本。我最常遇到的原因是可读性、效率或减少行数.
因为我原来的例子太复杂了,这里有一个(希望如此)非常基本的例子:
type KeyList = 'list' | 'of' | 'some' | 'keys';
// Original type (e.g. loaded from a JSON file)
interface Mappable {
source: { [K in KeyList]: SomeNestedObject },
sourceOrder: KeyList[];
}
// Mapped Type (mapped for easier access)
interface Mapped {
source: { [K in KeyList]: SomeNestedObject },
sourceOrder: SomeDeepObject[];
}
// What I have to do to keep suggestions and strict types all the way
const json: Mappable = JSON.parse(data); // ignoring validation for now
const mapped: Mapped = toMappedData(json);
// What I would like to to
const mapped: Mappable = JSON.parse(data);
mapData(mapped); // mapped is now of type Mapped
原因,为什么我想改变对象属性和它的类型可能是:
json
对象非常大,在内存中保存 2 个副本会适得其反- 创建
json
对象的深拷贝到mapped
对象中非常麻烦
我不认为“我想做什么”下的代码可读性很好,不管它是否工作。 我正在寻找一种干净且类型安全的方法来解决此问题。或者,或者,建议或想法来扩展 typescript 的功能以解决此问题。
非常感谢对此的任何建议、想法和评论!也许我对此太深入了,看不到真正简单的解决方案。
我目前的做法是:
type KeyList = 'list' | 'of' | 'some' | 'keys';
// Merged the types to keep the code a little shorter
// Also makes clear, that I don't alter the type's structure
interface MyType<M ext boolean = true> {
source: { [K in KeyList]: SomeNestedObject },
sourceOrder: (M ? SomeNestedObject : (KeyList | SomeNestedObject))[];
}
function mapData(data: MyType<true>): MyType {
const { source, sourceOrder } = data.sourceOrder
for (let i = 0; i < sourceOrder.length; i++) {
if (typeof sourceOrder[i] == 'string') {
sourceOrder[i] = source[sourceOrder[i]];
}
}
return (data as unknown) as MyType;
}
const json: MyType<true> = JSON.parse(data);
const mapped: MyType = mapData(json);
// mapped now references json instead of being a clone
我不喜欢这种方法的地方:
- 它不是类型安全的。打字稿无法检查我是否正确地改变了类型
- 因此我必须转换为未知,这并不理想
json
类型不那么严格。可能会对代码建议产生负面影响- 该函数有一个
side effect
和一个return type
(只有side effect
或return type
会更干净)
我不认为你想做的是可能的。您要求打字稿更改变量类型作为副作用。但这会带来各种并发症。
如果 mapData
函数是 运行 有条件的怎么办?
const mapped: Mappable = JSON.parse(data);
if (Math.random() > 0.5) {
mapData(mapped); // mapped is now of type Mapped
}
// What type is `mapped` here?
或者,如果您在转换之前传递对此对象的引用会怎样?
function doAsyncStuff(obj: Mappable) {}
doAsyncStuff(mapped)
mapData(mapped)
// Then later, doAsyncStuff(obj) runs but `obj` is a different type than expected
我认为你能在这里得到的最接近的是类型保护,它转换为中间类型,支持预转换类型和 post-transform 类型的联合,你可以在其中实际进行转换.
interface A {
foo: string
}
interface B {
foo: string
bar: string
}
interface A2B {
foo: string
bar?: string
}
function transform(obj: A): obj is B {
const transitionObj: A2B = obj
transitionObj.bar = "abc" // Mutate obj in place from type A to type B
return true
}
const obj: A = { foo: 'foo' }
if (transform(obj)) {
obj // type is B in this scope
}
obj // but obj is still type A here, which could lead to bugs.
但是,如果您实际上在该条件之外使用 obj
作为类型 A
,那么这可能会导致 运行 时间错误,因为类型是错误的。所以带有副作用的类型保护函数也是一个非常糟糕的主意,因为类型保护可以让你覆盖类型脚本的正常输入。
我真的认为您已经在这里做了最好的方法,尤其是 如果输出类型与输入类型不同。将新对象不可变地构造为新类型。
const mapped: Mapped = toMappedData(json);
如果性能或内存是一个大问题,那么您可能不得不为此牺牲类型安全性。编写健壮的单元测试,将其投射到任何单元测试,添加关于那里发生的事情的非常突出的评论。但是除非您一次处理数百 MB 的数据,否则我敢打赌这真的没有必要。