定义一个部分类型的形状/对象

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

Defining a partially typed Shape/Object

问题

有没有一种方法只定义对象类型的“一部分”,同时使其余部分基本上是“any”类型?主要目标是让intelliSense支持已定义的部分,但如果包括类型检查支持,那就更好了。为了说明问题,让我们首先定义一个辅助类型:

type TupleToIntersection<T extends any[]> = {
  [I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer U) => void
  ? U
  : never

它做了名字所示的事情,现在让我们处理这个问题:

type A = {
  A: { name: "Foo" }
}
type B = {
  B: { name: "Bar" }
}

type Collection<T extends any[]> = TupleToIntersection<{ [I in keyof T]: T[I] }>

declare const C: Collection<[A, B]>

C.A.name // "Foo" as expected
C.B.name // "Bar" as expected

declare const D: Collection<[A, any]>

D // <=== this is now of any type because obviously an intersection of any and type A gives back any
D.A.name // <=== So whilst this is technically still okay, there is no "intelliSense" support

有没有办法实现这一点?

我想到的一个主意是将类型保持为“any”,然后在代码中使用它的地方使用类型保护来强制将其转换为我知道其形状的类型,这是我认为tsc实际上想要将其保持为“类型安全”代码的方式,但这需要“不必要”的类型保护定义以及在需要使用它们的地方定义类型,而不是由系统“为您”定义。

英文:

Is there a way to define only a "part" of the object type, while having the rest of it essentially be "any" type? The goal main goal here is to have intelliSense support for the part that is defeined, but if types-checking support is included, all the better. To illustrate, lets first define one helper type:

type TupleToIntersection&lt;T extends any[]&gt; = {
  [I in keyof T]: (x: T[I]) =&gt; void
}[number] extends (x: infer U) =&gt; void
  ? U
  : never

It does what the name implies, now let's deal with the issue

type A = {
  A: { name: &quot;Foo&quot; }
}
type B = {
  B: { name: &quot;Bar&quot; }
}
    
type Collection&lt;T extends any[]&gt; = TupleToIntersection&lt;{ [I in keyof T]: T[I] }&gt;

declare const C: Collection&lt;[A, B]&gt;

C.A.name // &quot;Foo&quot; as expected
C.B.name // &quot;Bar&quot; as expected

declare const D: Collection&lt;[A, any]&gt;

D // &lt;=== this is now of any type because obviously an intersection of any and type A gives back any
D.A.name // &lt;=== So whilst this is technically still okay, there is no &quot;intelliSense&quot; support

Any way to achieve this?

One idea I had was to keep the type as "any" and then have a typeguard to force it into a type that I know the shape of, where I am using it in code, which I think is the way that tsc actually wants to keep it as a "typesafe" code, but this requires "unnecessary" defning of a typeguard as well as defining types where you need to use them instead of having them defined "for you" by the system

答案1

得分: 1

以下是您要翻译的内容:

交集类似于数学集合交集,其中A & B的结果是来自AB的所有元素。

any是一个包含所有可能值的集合,因此与any进行交集运算将在最后得到any

// any
type Case1 = number & any;
// any
type Case2 = { a: string } & any;

要修复它,您可以用unknown替换any,它类似于any,但编译器会优先考虑另一个集合的类型:

// number
type Case1 = number & unknown;
// { a: string }
type Case2 = { a: string } & unknown;

测试:

declare const D: Collection<[A, unknown]>;

D; // A
D.A.name // "Foo"

但如果您想支持将一些额外内容添加到类型中,我建议您避免使用anyunknown,可以使用更具体的东西。例如,要支持向对象添加新字段,可以使用Record<string, any>

测试:

declare const D: Collection<[A, Record<string, any>]>;

D; // A & Record<string, any>
D.A.name; // "Foo"
D.additional = ''; // 没有错误

请注意,代码中的&amp;&quot;已被还原为正常的&"

英文:

The intersection is similar to the mathematical set intersection, where the result of A &amp; B are all elements from A and B.

any is a set that contains every possible value so intersecting with any will give any at the end:

// any
type Case1 = number &amp; any;
// any
type Case2 = {a: string} &amp; any;

To fix it you can replace any with unknown, which is similar to any, however, the compiler will give preference to the type from the other set:

// number
type Case1 = number &amp; unknown
// {a: string}
type Case2 = {a: string} &amp; unknown;

Testing:

declare const D: Collection&lt;[A, unknown]&gt;;

D; // A
D.A.name // &quot;Foo&quot;

But if you want to support something extra to be added to the type, which I encourage you to avoid, you can use something more specific rather than any or unknown. For instance, to support adding new fields to an object one can use Record&lt;string, any&gt;

Testing:

declare const D: Collection&lt;[A, Record&lt;string, any&gt;]&gt;;

D; // A &amp; Record&lt;string, any
D.A.name; // &quot;Foo&quot;
D.additional = &#39;&#39;; // no error

答案2

得分: 1

这个方法对你有效吗?

/**
 * 由于某种原因,TypeScript 不会将字符串联合类型折叠为 `string`,
 * 如果它们通过 `(string & {})` 包括其中。这有点巧妙,我怀疑
 * 未来版本的 TypeScript 可能会破坏这种方法。
 */
type StringWithAutocomplete<U extends string> = U | (string & {});

/**
 * 此类型定义了你的智能提示,不包括通用的 `any` 属性
 */
type BaseType = {
  A: number;
  B: string;
  C: boolean;
}

/**
 * 对于给定的键,获取它在 `BaseType` 中的类型,否则解析为 `any`
 */
type KeyTypeOrAny<K extends StringWithAutocomplete<keyof BaseType>> =
  K extends keyof BaseType ?
    BaseType[K] :
    any;

/**
 * 这是一个映射类型,实际上是 `BaseType` 和 `Record<string, any>` 的交集,
 * 但具有已定义类型和智能提示的类型检查。
 */
type AnyWithAutocomplete = {
  [key in StringWithAutocomplete<keyof BaseType>]: KeyTypeOrAny<key>;
};

const foo: AnyWithAutocomplete = {
  A: 3,
  B: 'string',
  C: true,
  D: 'undefined types can be anything',
};

TypeScript Playground

在我进一步详细说明之前,作为一种必要的警告,要非常小心允许 any 渗入你的代码库。任何触及 any 类型的代码基本上都放弃了 TypeScript 的类型检查,因此在可能的情况下最好尽量避免使用它。


正如我在代码示例的注释中所解释的那样,这种方法依赖于 TypeScript 中的一种我怀疑是相当巧妙的方式,允许任何字符串同时保留自动完成功能。

通常,包括 string 的字符串联合类型,例如 &#39;A&#39; | &#39;B&#39; | string,会折叠成 string。然而,如果你将该联合类型更改为 &#39;A&#39; | &#39;B&#39; | (string &amp; {}),TypeScript 将不会折叠该联合类型,因此保留了各个字符串,同时也允许任何字符串。

{} 类型包括除了 nullundefined 之外的所有内容。实际上,JavaScript 中可以访问属性而不会引发错误的所有内容都包括在其中 - 只有 nullundefined 是其中的例外。内置实用工具 NonNullable 类型实际上是使用与此 {} 类型的交集实现的,这也是为什么通常会受到 linters 警告的原因,因为它可能看起来只是表示 "任何对象"。

无论如何,我已经利用了这个特性来创建了一个映射类型,其中包含一些特定的属性,同时允许任何字符串属性。通过定义一个将这些命名属性与类型关联起来的类型,对于其他属性则回退到 any,这使我们能够创建一种类型,它实际上是一个明确定义的对象类型和 Record<string, any> 的交集,而不会导致 TypeScript 将该交集

英文:

Does this approach work for you?

/**
 * For some reason, TypeScript won&#39;t collapse string unions down to `string`
 * if they include it via `(string &amp; {})`. This is a bit hacky, and I suspect
 * it may break in future versions of TypeScript.
 */
type StringWithAutocomplete&lt;U extends string&gt; = U | (string &amp; {});

/**
 * This type defines your intellisense, excluding the catch-all `any` properties
 */
type BaseType = {
  A: number;
  B: string;
  C: boolean;
}

/**
 * For a given key, get its type in `BaseType` or otherwise resolve to `any`
 */
type KeyTypeOrAny&lt;K extends StringWithAutocomplete&lt;keyof BaseType&gt;&gt; =
  K extends keyof BaseType ?
    BaseType[K] :
    any;

/**
 * A mapped type that is functionally an intersection of `BaseType` and
 * `Record&lt;string, any&gt;`, but with type-checking on defined types and
 * intellisense.
 */
type AnyWithAutocomplete = {
  [key in StringWithAutocomplete&lt;keyof BaseType&gt;]: KeyTypeOrAny&lt;key&gt;;
};

const foo: AnyWithAutocomplete = {
  A: 3,
  B: &#39;string&#39;,
  C: true,
  D: &#39;undefined types can be anything&#39;
};

TypeScript Playground

Just as an obligatory cautionary note, before I go into the details, be very careful with how you allow any to leak into your codebase. Any code that touches the any type is essentially opting out of TypeScript's type checking, so it's generally best avoided whenever possible.


As I've explained in the comments in the code example, this approach hinges on what I suspect is a rather hacky way in TypeScript to allow any string while retaining autocomplete.

Normally, any union of strings that includes string, for example &#39;A&#39; | &#39;B&#39; | string, collapses down to just string. However, if you change that union to &#39;A&#39; | &#39;B&#39; | (string &amp; {}) then TypeScript will not collapse the union, so it retains the individual strings while also allowing any string.

The {} type includes everything except for null or undefined. Essentially, everything in JavaScript on which properties can be accessed without throwing an error - null and undefined are the only exceptions to that. The built-in utility NonNullable type is actually implemented using an intersection with this {} type, and this is also why it's typically warned against by linters since it may look like it just means "any object".

Anyway, I've used this quirk to create a mapped type that has some specific properties, while also allowing any string property. By defining a type which associates those named properties with a type, while falling back to any for other properties, this lets us create a type that is essentially an intersection between a well-defined object type and Record&lt;string, any&gt;, without causing TypeScript to collapse that intersection to simply any. This means you get to keep your type checking and autocomplete, but also have the open-ended type you were looking for.

huangapple
  • 本文由 发表于 2023年7月17日 18:25:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76703534.html
匿名

发表评论

匿名网友

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

确定