处理 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

原因,为什么我想改变对象属性和它的类型可能是:

我不认为“我想做什么”下的代码可读性很好,不管它是否工作。 我正在寻找一种干净且类型安全的方法来解决此问题。或者,或者,建议或想法来扩展 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 effectreturn 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 的数据,否则我敢打赌这真的没有必要。