如何在已初始化的对象上有条件地添加属性,而无需手动键入?

huangapple go评论137阅读模式
英文:

How to conditionally add properties to an object that was initialized already, without typing it?

问题

我可以这样修复这个问题:

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
  position?: string;
}
const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
} as TPerson;

但是否有可能绕过这个问题呢?类似这样的方式:

const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
} as this { position?: string; };
英文:

Suppose the following:

const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
};

if (1 + 1 === 2) {
  person.position = 'Sales Clerk'; // error here
}

I can fix this as follows:

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
  position?: string;
}
const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
} as TPerson;

But is it possible to somehow circumvent this? Something along the lines of,

const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
} as this { position?: string; };

答案1

得分: 1

以下是翻译的内容:

现有类型可以通过使用返回类型断言和/或断言函数的函数来缩小。

下面我将解释如何使用它们来实现您所提出的目标,但首先,解决问题的惯用且最简单的方法是创建一个具有正确的类型注解的新变量,并将其值设置为现有对象:

TS Playground

type Person = {
  name: string;
  age: number;
  hobbies: string[];
  position?: string;
};

const person = {
  name: "George",
  age: 30,
  hobbies: ["art", "gaming"],
};

const actualPerson: Person = person;

if (1 + 1 === 2) {
  actualPerson.position = "Sales Clerk"; // ok
}

这不会在内存中创建一个新对象,只会创建一个新变量并引用相同的对象,赋值成功是因为现有对象缺少 position 属性,这与 { position?: string } 兼容("一个可选的字符串属性")。

除非您已经在现有作用域中用尽了所有合理的变量名,这是最简单、最可读和性能最佳的方法。

现在,继续…


现有的 person 对象没有 position 属性,编译器正确地推断出这一点,并在尝试访问或设置它时发出错误诊断:

TS Playground

const person = {
  name: "George",
  age: 30,
  hobbies: ["art", "gaming"],
};

person.position; /* Error
       ~~~~~~~~
属性 'position' 不存在于类型 '{ name: string; age: number; hobbies: string[]; }' 中。(2339) */

TypeScript 提供的一种缩小类型的功能是一个返回类型断言的函数,通常被称为用户定义的类型守卫。

在下面的代码中,我创建了这样一个函数,该函数在运行时验证参数对象要么没有 position 属性,要么如果有的话,属性值的类型是 undefinedstring。这是一个不完美但直观的检查,以确定它当前是否具有可以被视为与 { position?: string } 兼容的形状。 (请参阅[此处的说明](https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&target=99&jsx=4&useUnknownInCatchVariables=true&exactOptionalPropertyTypes=true#code/GYVwdgxgLglg9mABACwIYGcBycAKd0ywIDyATgMpSkxgDmeBRYAPACqICmAHlB2ACbpEcAEYArDtAB8ACgBQiYeIBciVgBo5ASlWixiGEPYAyRAG9EAB3yF4YAPyr0VGrUQBfcwoPBE8xYoAhDIARNaMdiEGSHpa3ooAPgmIUACelhxwvnoAdOG2CIgAvCWIIeD8HMA0HPwh8YhJKemZ2eJ5NkzFpSHO1HT

英文:

An existing type can be narrowed using a function which returns a type predicate and/or an assertion function.

Below I'll explain how to use them to achieve your stated goal, but first — the idiomatic and simplest way to solve the problem is just to create a new variable with the correct type annotation and set its value to the existing object:

TS Playground

type Person = {
  name: string;
  age: number;
  hobbies: string[];
  position?: string;
};

const person = {
  name: "George",
  age: 30,
  hobbies: ["art", "gaming"],
};

const actualPerson: Person = person;

if (1 + 1 === 2) {
  actualPerson.position = "Sales Clerk"; // ok
}

This doesn't create a new object in memory — just a new variable and reference to the same object — and the assignment succeeds because the existing object lacks a position property, which is compatible with { position?: string } ("an optional string property").

Unless you've somehow already exhausted all reasonable variable names in your existing scope, this is the simplest, most readable and performant method.

Now, onward...


The existing person object doesn't have a position property — the compiler correctly infers this and helpfully emits an error diagnostic when attempting to access or set it:

TS Playground

const person = {
  name: "George",
  age: 30,
  hobbies: ["art", "gaming"],
};

person.position; /* Error
       ~~~~~~~~
Property 'position' does not exist on type '{ name: string; age: number; hobbies: string[]; }'.(2339) */

One narrowing feature that TypeScript offers is a function which returns a type predicate — you'll hear this often referred to as a user-defined type guard.

In the code below, I've created such a function which validates at runtime that the argument object either has no position property, or that — if it does — the type of the property value is either "undefined" or "string". This is an imperfect, but intuitive check to determine whether or not it is currently of a shape that might be considered compatible with { position?: string }. (See caveat here)

function hasNoPositionOrStringPosition<T extends object>(
  obj: T,
): obj is T & { position?: string } {
  if (
    !("position" in obj)
    || typeof obj.position === "undefined"
    || typeof obj.position === "string"
  ) return true;
  return false;
}

It can then be used as part of a conditional clause at runtime, like this:

const person = {
  name: "George",
  age: 30,
  hobbies: ["art", "gaming"],
};

if (hasNoPositionOrStringPosition(person)) {
  person.position = "Sales Clerk"; // ok
       //^? (property) position?: string
}

person.position; /* Error
       ~~~~~~~~
Property 'position' does not exist on type '{ name: string; age: number; hobbies: string[]; }'.(2339) */

If the function returns true, then the compiler is informed that the argument is now the type in the predicate: so the compiler now understands the person object to have an optional string property called position while inside the conditional block.

But as you can see above, that type information only applies inside the conditional block. As soon as the block scope ends, the previous type is again in effect, and the compiler no longer recognizes the position property.

This technique is safe and offers powerful, granular control. But what if you want to narrow the type for the rest of the lifetime of the variable in the outer scope?

That's where assertion functions come into play.

> By the way, be sure to read all of the linked documentation pages in this answer!

In the code below I show a generalized assertion function assert that accepts some expression expr, and optionally a message string msg:

function assert(expr: unknown, msg?: string): asserts expr {
  if (!expr) throw new Error(msg);
}

If the expression is evaluated NOT to be truthy, then the function throws an error (using the message provided). Otherwise, the function completes without any issue. In the case that the error is not thrown, the compiler will infer whatever type information is in the expression expr and apply it at the current scope.

Here are a couple of examples:

TS Playground

let value = "hello" as string | number;

value
//^? let value: string | number

assert(typeof value === "string", "Value was not a string");

// If the assertion above didn't throw an exception,
// now the compiler is certain that `value` is of type `string`:
value
//^? let value: string

console.log(value); //=> "hello"

However, in the case that value isn't a string...

TS Playground

let value = 5 as string | number;

value
//^? let value: string | number

assert(typeof value === "string", "Value was not a string");
// An exception is thrown here -> Error: Value was not a string

// The assertion above will throw an exception,
// so this code won't be evaluated at runtime:
value
//^? let value: string

console.log(value); // Also won't run because the assertion threw an exception

Now that you've seen basic usages of both function types, it's time to combine them together:

TS Playground

function assert(expr: unknown, msg?: string): asserts expr {
  if (!expr) throw new Error(msg);
}

function hasNoPositionOrStringPosition<T extends object>(
  obj: T,
): obj is T & { position?: string } {
  if (
    !("position" in obj)
    || typeof obj.position === "undefined"
    || typeof obj.position === "string"
  ) return true;
  return false;
}

const person = {
  name: "George",
  age: 30,
  hobbies: ["art", "gaming"],
};

person.position; /* Error
       ~~~~~~~~
Property 'position' does not exist on type '{ name: string; age: number; hobbies: string[]; }'.(2339) */

assert(hasNoPositionOrStringPosition(person));

person.position; // ok
console.log(person.position); //=> undefined
                 //^? (property) position?: string | undefined

if (1 + 1 === 2) {
  person.position = "Sales Clerk"; // ok
       //^? (property) position?: string
}

console.log(person.position); //=> "Sales Clerk"

In the code above, person is asserted to have either no position property or one with an undefined or string value. So — for the rest of the program scope — the compiler now thinks that the person object's position property is string | undefined.

> Technically, the property is (string | undefined) & typeof person["position"], but since person had no position property, it's just string | undefined.

Hopefully this illustrates how to accomplish your goal as stated (but also why the "new variable with type annotation" method is a better approach).

答案2

得分: 0

如果允许更改“person”的定义,为什么不这样做?

const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
  position: undefined as string | undefined,
};

它可能不是最干净的解决方案,但它很简单并且有效。

[Playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBADgUwE4XDAvDA3gWAFAwxgCGAtggFwwDkA4giEgOYLUA0+hxLVAzAAwcCMABYgARuICWCCFQDa1YkijsaTMlLBNqAXSGE4ICFKhTwVAK5gAJggBmWhDZjEIMaEi1MYAHxjWdo5gzkIAvgDc+PhS9jAAFACMMADUMMnomTAATACU2JzwyKhgAHRGJmZomNQAysQANrIwAMJNSADW1BEwAPS9MADyANL4YUA)
英文:

If you're allowed to change the definition of person, why not

const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
  position: undefined as string | undefined,
};

It may not be the cleanest but it's simple and it works.

Playground

答案3

得分: 0

只需将您的类型断言更改为类型注释,对我来说看起来很不错。

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
  position?: string;
}
const person: TPerson = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
} 

if (1 + 1 == 2) person.position = 'Clark Kent';

或者,如果您希望将 position 命名为其他任何名称,可以这样做。

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
} & Record<string, string | number | string[]>

或者像这样:

type Position = {
  position?: string;
}

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
}

type TPersonWithPosition = TPerson & Position;
英文:

Just change your type assertion to type annotation and it looks good to me.


type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
  position?: string;
}
const person: TPerson = {
  name: &#39;George&#39;,
  age: 30,
  hobbies: [&#39;art&#39;, &#39;gaming&#39;],
} 

if(1+1==2) person.position = &#39;Clark Kent&#39;

Or do like this if you want position to be named anything.

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
}  &amp; Record&lt;string, string | number | string[]&gt;

Or like this:

type Position = {
  position?: string
}

type TPerson = {
  name: string;
  age: number;
  hobbies: string[];
} 


type TPersonWithPosition = TPerson &amp; Position

答案4

得分: 0

你不能以不允许的方式更改对象的类型。

但你总是可以从旧对象中创建一个新对象:

const person = {
  name: 'George',
  age: 30,
  hobbies: ['art', 'gaming'],
}

const personWithPosition = {
  ...person,
  position: 'Sales Clerk',
} // 这个对象及其类型现在具有一个`position`属性。
英文:

You can't change an object in a way that it's type does not allow.

But you can always make a new object from the old one:

const person = {
  name: &#39;George&#39;,
  age: 30,
  hobbies: [&#39;art&#39;, &#39;gaming&#39;],
}

const personWithPosition = {
  ...person,
  position: &#39;Sales Clerk&#39;,
} // this object, and it&#39;s type now has a `position` property.

huangapple
  • 本文由 发表于 2023年3月4日 04:23:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/75631566.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定