如何在打字稿中选择性地从一个 Partial 分配到另一个 Partial [英] How to selectively assign from one Partial to another in typescript

查看:26
本文介绍了如何在打字稿中选择性地从一个 Partial 分配到另一个 Partial的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在下面的 TypeScript 代码片段中,我需要从一个对象分配到另一个对象,其中它们都是 Partial.在这里,我的直觉是打字稿应该能够理解正在发生的事情,因为在第 (B) 行,key 的类型是 typeof InjectMap.因此,它应该能够正确地将值从 input 分配给 output.

<预><代码>导出接口 InjectMap {"A": "B",//行 (A)C":D"}type InjectKey = keyof InjectMap;const 输入:Partial= {};const 输出:Partial= {};const 键:InjectKey[] = []for (let i = 0; i

游乐场链接

但它在第 (C) 行给出了以下错误:

输入B"|D"|undefined' 不能分配给类型 'undefined'.输入B"不可分配给类型未定义".

奇怪的是,如果我注释 (A) 行,错误就会消失.这是 TypeScript 的缺点还是我遗漏了什么?

解决方案

我不认为这是一个错误,改变值几乎总是不安全的,而 TS 只是试图使其安全.

让我们从 InjectMap 接口开始.

很明显,你不能有像这样的非法状态:

const 非法:InjectMap = {A":D",//预期 BC":B"//预期 D}

这很重要.

让我们继续我们的循环:

interface InjectMap {A":B",C":D"}type InjectKey = keyof InjectMap;const 输入:Partial= {};const 输出:Partial= {};const 键:InjectKey[] = []for (let i = 0; i 

因为key是动态的,TS不确定是BD还是undefined.我希望你同意我的看法,在这个地方 inp 的正确类型是 "B"|D"|未定义,这是预期的行为,因为类型系统是静态的.

由于inputoutput不是由key绑定的,TS想要避免非法状态.为了清楚起见,请考虑下一个示例,它等于我们的

type KeyType_ = "B";|D"|不明确的让 keyB: KeyType_ = 'B';让 keyD: KeyType_ = 'D'output[keyB] = input[keyD]//Boom,非法状态!运行时错误!

您可能已经注意到,keyBkeyD 具有相同的类型但具有不同的值.

与示例中的情况相同,TS 无法确定它只能确定类型的值.

如果你想让 TS 开心,你应该添加条件语句或类型保护:

for (let i = 0; i 

请记住,当你改变你的值时,你会失去类型保证.

thisthis 关于突变的问题.

此外,Titian Dragomir Cernicova 的谈话也很不错.

这里有一个不安全突变的例子,摘自@Titian 的演讲:

type Type = {名称:字符串}类型 SubTypeA = 类型 &{薪水:字符串}类型 SubTypeB = 类型 &{汽车:布尔值}类型扩展<T,U>=T 扩展 U ?真假让员工:SubTypeA = {name: 'John Doe',薪水:'1000$'}让人类:类型 = {名称:摩根弗里曼"}让导演:SubTypeB = {name: '将',汽车:真实}//同一方向类型协方差 T= {框:T}let employeeInBox: Covariance= {框:员工}let humanInBox:协方差<类型>= {盒子:人类}//突变 ob 对象属性让测试:协方差<类型>= 员工收件箱test.box = Director//employeeInBox 的变异const result_ = employeeInBox.box.salary//当 result_ 未定义时,它被推断为一个字符串//数组的变异让数组:数组<类型>= []让员工 = [员工]数组 = 员工array.push(导演)const result = Employees.map(elem => elem.salary)//当薪水是 [string, undefined] 时,被推断为 string[]console.log({result_,result})

游乐场

如何解决?

请告诉我它是否适合您:

导出接口 InjectMap {A":B",C":D"}constassign = <输入扩展InjectMap,输出扩展InjectMap>(输入:部分<输入>,输出:部分<输出>,键:Array) =>keys.reduce((acc, elem) => ({...acc,[元素]:输入[元素]}), 输出)

游乐场

更新

<块引用>

为什么这个分析适用于[elem]:input[elem]?input[elem] 可以再次是B"|D"|未定义,因此编译器应该再次给出错误.但是,这里的编译器很聪明地知道 input[elem] 的类型适用于 [elem].有什么区别?

这是一个很好的问题.

当你用计算键创建新对象时,TS 使这个对象按字符串索引,我的意思是,你可以使用任何你想要的字符串prop

const calculateProperty = (prop: keyof InjectMap) =>{常量结果 = {[prop]: 'some prop'//{ [x: string]: string;}}返回结果}

它给了你更多的自由,但也带来了一些不安全.

<块引用>

能力越大责任越大

因为现在,不幸的是,您可以这样做:

const assign = <Input extends InjectMap, Output extends InjectMap>(输入:部分<输入>,输出:部分<输出>,键:Array) =>keys.reduce((acc, elem) => {返回 {...acc,[elem]: 1//不安全的行为}}, 输出)

您可能已经注意到,assign 函数的返回类型是 Partial,这是不正确的.

因此,为了使其完全类型安全,您可以使用 typeguards,但我认为它会过于复杂

In the TypeScript code snippet below, I need to assign from one object to another where both of them are Partial<InjectMap>. Here, my intuition says that typescript should be able to understand what is going on because at line (B), the type of key is typeof InjectMap. So, it should be able to assign values from input to output correctly.


export interface InjectMap {
    "A": "B",                          // line (A)
    "C": "D"
}
type InjectKey = keyof InjectMap;

const input: Partial<InjectMap> = {};
const output: Partial<InjectMap> = {};
const keys: InjectKey[] = []

for (let i = 0; i < keys.length; i++) {
    const key = keys[i]                // line (B)
    output[key] = input[key]           // line (C)  - Gives Error
}

Playground Link

But it gives the following error at line (C):

Type '"B" | "D" | undefined' is not assignable to type 'undefined'.
  Type '"B"' is not assignable to type 'undefined'.

Strangely, the error goes away if I comment line (A). Is this a shortcoming in TypeScript or am I missing something?

解决方案

I don't think it is a bug, it is almost always unsafe to mutate the values and TS just tries to make it safe.

Let's start from InjectMap interface.

It is clear that you cant have illegal state like:

const illegal: InjectMap = {
    "A": "D", // expected B
    "C": "B" // expected D
}

This is important.

Let's proceed with our loop:

interface InjectMap {
    "A": "B",
    "C": "D"
}
type InjectKey = keyof InjectMap;

const input: Partial<InjectMap> = {};
const output: Partial<InjectMap> = {};

const keys: InjectKey[] = []


for (let i = 0; i < keys.length; i++) {
    const key = keys[i];

    const inp = input[key] // "B" | "D" | undefined
    const out = output[key] // "B" | "D" | undefined

    output[key] = input[key]
    
}

Because key is dynamic, TS is unsure whether it B, D or undefined. I hope that you are agree with me, that in this place correct type of inp is "B" | "D" | undefined, it is expected behavior, because type system is static.

Since, input and output are not binded by key, TS wants to avoid illegal state. To make it clear, consider next example, which is equal to our

type KeyType_ = "B" | "D" | undefined

let keyB: KeyType_ = 'B';
let keyD: KeyType_ = 'D'

output[keyB] = input[keyD] // Boom, illegal state! Runtime error!

As you might have noticed, keyB and keyD have the same type but different values.

Same situation you have in your example, TS is unable to figure out the value it is able to figure out only type.

If you want to make TS happy, you should add condition statement or typeguard:

for (let i = 0; i < keys.length; i++) {
    const key = keys[i];

    if (key === 'A') {
        let out = output[key] // "B"
        let inp = input[key] //  "B"

        output[key] = input[key] // ok
    }

    if (key === 'C') {
        let out = output[key] // "D"
        let inp = input[key] //  "D"

        output[key] = input[key] // ok
    }
}

Please keep in mind, when you mutate your values, you loose type guaranties.

See this and this question about mutations.

Also this talk of Titian Dragomir Cernicova is pretty good.

Here you have an example of unsafe mutation, taken from @Titian 's talk:

type Type = {
    name: string
}

type SubTypeA = Type & {
    salary: string
}

type SubTypeB = Type & {
    car: boolean
}

type Extends<T, U> =
    T extends U ? true : false


let employee: SubTypeA = {
    name: 'John Doe',
    salary: '1000$'
}

let human: Type = {
    name: 'Morgan Freeman'
}

let director: SubTypeB = {
    name: 'Will',
    car: true
}


// same direction
type Covariance<T> = {
    box: T
}

let employeeInBox: Covariance<SubTypeA> = {
    box: employee
}

let humanInBox: Covariance<Type> = {
    box: human
}

// Mutation ob object property
let test: Covariance<Type> = employeeInBox

test.box = director // mutation of employeeInBox

const result_ = employeeInBox.box.salary // while result_ is undefined, it is infered a a string


// Mutation of Array
let array: Array<Type> = []
let employees = [employee]
array = employees
array.push(director)

const result = employees.map(elem => elem.salary) // while salary is [string, undefined], is is infered as a string[]

console.log({result_,result})

Playground

How to fix it ?

Please, let me know if it works for you:


export interface InjectMap {
    "A": "B",
    "C": "D"
}

const assign = <Input extends InjectMap, Output extends InjectMap>(
    input: Partial<Input>,
    output: Partial<Output>,
    keys: Array<keyof InjectMap>
) => keys.reduce((acc, elem) => ({
    ...acc,
    [elem]: input[elem]
}), output)

Playground

UPDATE

why does this analysis apply for [elem]: input[elem]? input[elem] can again be "B"|"D"|undefined and thus the compiler should give error again. But, here compiler is intelligent to know that the type of input[elem] applies for [elem]. What is the difference?

That is a very good question.

When you create new object with computed key, TS makes this object indexed by string, I mean, you can use any string prop you want

const computedProperty = (prop: keyof InjectMap) => {
    const result = {
        [prop]: 'some prop' //  { [x: string]: string; }
    }

    return result
}

It gives you more freedom but also provides a bit of unsafety.

With great power comes great responsibility

Because now, unfortunately, you can do this:

const assign = <Input extends InjectMap, Output extends InjectMap>(
    input: Partial<Input>,
    output: Partial<Output>,
    keys: Array<keyof InjectMap>
) => keys.reduce((acc, elem) => {  

    return {
        ...acc,
        [elem]: 1 // unsafe behavior
    }

}, output)

As you might have noticed, return type of assign function is Partial<Output>, which is not true.

Hence, in order to make it completely type safe you can use with typeguards, but I think it will be overcomplicating

这篇关于如何在打字稿中选择性地从一个 Partial 分配到另一个 Partial的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆