英文:
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<T extends any[]> = {
[I in keyof T]: (x: T[I]) => void
}[number] extends (x: infer U) => void
? U
: never
It does what the name implies, now let's deal with the issue
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 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
的结果是来自A
和B
的所有元素。
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"
但如果您想支持将一些额外内容添加到类型中,我建议您避免使用any
或unknown
,可以使用更具体的东西。例如,要支持向对象添加新字段,可以使用Record<string, any>
。
测试:
declare const D: Collection<[A, Record<string, any>]>;
D; // A & Record<string, any>
D.A.name; // "Foo"
D.additional = ''; // 没有错误
请注意,代码中的&
和"
已被还原为正常的&
和"
。
英文:
The intersection is similar to the mathematical set intersection, where the result of A & 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 & any;
// any
type Case2 = {a: string} & 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 & unknown
// {a: string}
type Case2 = {a: string} & unknown;
Testing:
declare const D: Collection<[A, unknown]>;
D; // A
D.A.name // "Foo"
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<string, any>
Testing:
declare const D: Collection<[A, Record<string, any>]>;
D; // A & Record<string, any
D.A.name; // "Foo"
D.additional = ''; // 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',
};
在我进一步详细说明之前,作为一种必要的警告,要非常小心允许 any
渗入你的代码库。任何触及 any
类型的代码基本上都放弃了 TypeScript 的类型检查,因此在可能的情况下最好尽量避免使用它。
正如我在代码示例的注释中所解释的那样,这种方法依赖于 TypeScript 中的一种我怀疑是相当巧妙的方式,允许任何字符串同时保留自动完成功能。
通常,包括 string
的字符串联合类型,例如 'A' | 'B' | string
,会折叠成 string
。然而,如果你将该联合类型更改为 'A' | 'B' | (string & {})
,TypeScript 将不会折叠该联合类型,因此保留了各个字符串,同时也允许任何字符串。
{}
类型包括除了 null
或 undefined
之外的所有内容。实际上,JavaScript 中可以访问属性而不会引发错误的所有内容都包括在其中 - 只有 null
和 undefined
是其中的例外。内置实用工具 NonNullable
类型实际上是使用与此 {}
类型的交集实现的,这也是为什么通常会受到 linters 警告的原因,因为它可能看起来只是表示 "任何对象"。
无论如何,我已经利用了这个特性来创建了一个映射类型,其中包含一些特定的属性,同时允许任何字符串属性。通过定义一个将这些命名属性与类型关联起来的类型,对于其他属性则回退到 any
,这使我们能够创建一种类型,它实际上是一个明确定义的对象类型和 Record<string, any>
的交集,而不会导致 TypeScript 将该交集
英文:
Does this approach work for you?
/**
* For some reason, TypeScript won't collapse string unions down to `string`
* if they include it via `(string & {})`. This is a bit hacky, and I suspect
* it may break in future versions of TypeScript.
*/
type StringWithAutocomplete<U extends string> = U | (string & {});
/**
* 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<K extends StringWithAutocomplete<keyof BaseType>> =
K extends keyof BaseType ?
BaseType[K] :
any;
/**
* A mapped type that is functionally an intersection of `BaseType` and
* `Record<string, any>`, but with type-checking on defined types and
* intellisense.
*/
type AnyWithAutocomplete = {
[key in StringWithAutocomplete<keyof BaseType>]: KeyTypeOrAny<key>;
};
const foo: AnyWithAutocomplete = {
A: 3,
B: 'string',
C: true,
D: 'undefined types can be anything'
};
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 'A' | 'B' | string
, collapses down to just string
. However, if you change that union to 'A' | 'B' | (string & {})
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<string, any>
, 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论