【问题标题】:How to assert matching types when iterating over an object?迭代对象时如何断言匹配类型?
【发布时间】:2021-10-20 02:38:14
【问题描述】:

下面的代码块会产生一个打字错误,因为虽然我们知道foo[k]bar[k]是同一类型,但TS无法知道(好吧,也许通过某种魔法可以,但是显然没有)

interface IMixed {
    a: number;
    b: string;
    c: boolean;
}

const foo: IMixed = { a: 1, b: 'one', c: true };

const bar: IMixed = { a: 2, b: 'two', c: false };

(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
    foo[k] = bar[k]; 
//  ^^^^^^ Type 'string | number | boolean' is not assignable to type 'never'.
});

当 TS 无法判断出我知道是真的某件事时,我会施放它。但是在这种情况下,虽然我知道这是一个有效的分配,但我不知道它是哪种类型......我只知道它们是相同的。我无法在这里找到一种优雅的方式来使用泛型。

假设我致力于迭代这些属性的做法(比如说,因为我们希望属性确实会添加到最后一行,并且类似的代码存在于整个代码库中......)

简而言之:我如何断言这个赋值是有效的?

(附带问题:为什么错误将受让人称为“从不”类型?)

更新

@captain-yossarian 对可变性提出了一个很好的观点,并提出了一个完全可行的不可变解决方案。不过,我觉得这个问题仍然悬而未决,如果这个例子有点诡计:

interface IMixed {
    a: number[];
    b: string[];
    c: boolean[];
}

const foo: IMixed = { a: [1], b: ['one'], c: [true] };
const bar: IMixed = { a: [2], b: ['two'], c: [false] };

function intersection<T>(...arrays: T[][]): T[] {
    return [...new Set([].concat(...arrays))]
}

(Object.keys(foo) as Array<keyof IMixed>)
  .reduce((acc, elem) => ({
    ...acc,
    [elem]: intersection(foo[elem], bar[elem])
                      // ^^^^^^^^^ Argument of type 'string[] | number[] | boolean[]' is not 
                      // assignable to parameter of type 'string[]'.
  }), foo);

关键是,赋值显然要求类型兼容......但intersection 函数也是如此。所以这就像原始问题的不可变版本。

【问题讨论】:

  • 想必您正在寻找比// @ts-ignore 更细致、更有帮助的东西;-)
  • 爱 @ts-ignore ;-) ...但 linter 不会让我 :)
  • @geofh 进行了更新。现在可以变异foo
  • 在上面的例子中k是一个联合'a' | 'b' | 'c'。为了缩小值类型 TS 需要 specific 键。您可以提取和使用接受特定 keyof IMixed typescriptlang.org/play?#code/… 的通用函数
  • never 表示您定义的类型不存在这种情况。比如尝试访问let a: number = 0;在 if 块中,例如 if(typeof a === 'string').

标签: typescript


【解决方案1】:

在这种情况下,值得使用reduce 而不是forEach

interface IMixed {
  a: number;
  b: string;
  c: boolean;
}

const foo: IMixed = { a: 1, b: 'one', c: true };

const bar: IMixed = { a: 2, b: 'two', c: false };

(Object.keys(foo) as Array<keyof IMixed>)
  .reduce((acc, elem) => ({
    ...acc,
    [elem]: bar[elem]
  }), foo);

Playground

Mutations 在 TypeScript 中效果不佳。

查看相关问题: firstsecondthirdforth 和我的article

更新

(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
    foo[k] = bar[k]; // error
});

这里有一个错误,因为forEachreduce 是动态的。 bar[k]number | string | booleanfoo[k] 的联合。这意味着在循环内部可以分配foo['a'] = foo['c']。这两个值都是有效的,因为我们期望 number | string | boolean 的联合,但它也是不安全的。这就是 TS 禁止这种行为的原因。

另一方面,reduce 有效,因为我们创建了扩展 IMixed 而不是变异的新对象

更新

为了能够改变foo,你需要将indexing添加到IMixed接口:

type IMixed= {
  a: number;
  b: string;
  c: boolean;
  [prop: string]: number | boolean | string
}

const foo: IMixed = { a: 1, b: 'one', c: true };

const bar: IMixed = { a: 2, b: 'two', c: false };


(Object.keys(foo) as Array<keyof IMixed>).forEach(k => {
  foo[k] = bar[k];
});

Playground

【讨论】:

  • 呃。我在最好的时候不喜欢reduce(在使用预定义、可重用、经过测试的reducer 进行函数式编程之外),但甚至没有使用它的结果值……blech。 :-) 真的没有更好的办法吗?
  • 我认为这是对reduce的合理使用,我同意关于不变性的说法。不幸的是,它只是因为我将示例简化(不是双关语)简化为简单的形式……在实际情况下,我不能那么容易地逃避这个问题。 @captain-yossarian 不知道如何解决这个问题,如果我更新问题以打败你的答案,这不会让你看起来很好:)
  • 谢谢!所以关键是创建一个新对象。您关于使用来自foo 的密钥的问题的观点是一个非常的好点(一如既往)。
  • 酷。 :-) 想在this 上戳洞吗? :-)
  • @captain-yossarian 我还没有回过头来编写一个更难解决的场景。但我确信这里的索引是一个禁忌......我从这种做法(其他人)中遭受了很多痛苦,因为它破坏了类型安全。你没找到吗?
【解决方案2】:

captain-yossarian 有用地(一如既往)指出您不能依赖 Object.keys(foo).forEach 来选择有效的密钥,因为它提供了 foo 的所有密钥,但 foo 可能是IMixed 具有属性 bar 没有。另外,仅仅因为foobar 都具有a 属性(例如)

似乎可以将循环基于目标 (foo) 的键并检查源 (bar) 中是否存在该键,同时要求它们通过通用参数共享一个公共子类型,像这样:

function copyAllProps<ObjectType>(target: ObjectType, source: ObjectType) {
    // The `as` below is a bit of a lie :-D
    (Object.keys(target) as Array<keyof ObjectType>).forEach(k => {
        // NOTE: The type of `k` here is slightly wrong. We told TypeScript
        // it's `keyof ObjectType`, but it could be something else, because
        // `target` can have keys that `source` doesn't have (`target`'s type can
        // be a supertype of `source`'s type). This `if` mitigates that by
        // checking that `source` does have the property.
        if (k in source) {
            target[k] = source[k];
        }
    });
}

(请注意其中的警告。)这只会复制公共属性。

用法示例:

// Your original examples
let foo: IMixed = { a: 1, b: 'one', c: true };
let bar: IMixed = { a: 2, b: 'two', c: false };

let bar2 = { a: 2, b: false, c: 'two'};

let foo3 = { a: 1, b: 'one', c: true, d: new Date() };
let bar3 = { a: 2, b: 'two', c: false, d: null };

function test() {
    copyAllProps(foo, bar);     // <== Works
    copyAllProps(foo, bar2);    // <== Error as desired, same keys but different types for `b` and `c`
    copyAllProps(foo3, bar3);   // <== Error as desired, same keys but different types for `d`
    copyAllProps(foo, {});      // <== Error as desired, source isn't a match
}

Playground link

【讨论】:

    【解决方案3】:

    这是更新问题的解决方案(不可变版本)......从@captain-yossarian 的答案和@AlekseyL 中汲取灵感。的评论。

    function intersection<K extends keyof IMixed>(k: K, mixed1: IMixed, mixed2: IMixed) {
        return [...new Set([].concat(mixed1[k], mixed2[k]))]
    }
    
    (Object.keys(foo) as Array<keyof IMixed>)
      .reduce((acc, elem) => {
        return {
            ...acc,
            [elem]: intersection(elem, foo, bar)
        } 
    }, foo);
    

    ...用更漂亮的东西概括如何:

    function pairwiseProcess<TIn, TOut>(
        item1: TIn,
        item2: TIn,
        iteratee: <K extends keyof TIn>(k: K, item1: TIn, item2: TIn) => TOut
    ) {
        return (Object.keys(item1) as Array<keyof TIn>).reduce((acc, key) => {
            return {
                ...acc,
                [key]: iteratee(key, item1, item2)
            };
        }, {});
    }
    
    console.log(pairwiseProcess(foo, bar, intersection));
    

    抱歉,尝试将其放入 https://www.typescriptlang.org/play,但它显示了我在 IDE 上看不到的错误(我什至可以使用 ts-node 运行它)...?

    【讨论】:

    • new Set([mixed1[k], mixed2[k]]) ?
    • new Set([...mixed1[k], ...mixed2[k]])怎么样
    • 啊,好吧,你也在处理数组?。那么[].concat(mixed1[k], mixed2[k])就可以了
    猜你喜欢
    • 2016-01-05
    • 2018-06-11
    • 1970-01-01
    • 2018-03-11
    • 1970-01-01
    • 2016-01-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多