条件类型和字符串文字 + class 联合交互
Conditional types and string literal + class union interaction
我在尝试正确键入 as
prop from emotion.
时遇到了类型系统中的奇怪交互
import React, { Component, FC, PropsWithChildren } from "react";
// Possible types for `as` prop are tag name or component
type AsType =
| keyof JSX.IntrinsicElements
| unknown
// Infered props based on `as`
type AsProps<T extends AsType> = {
as?: T;
} & (T extends FC<infer FProps>
? FProps & { fc?: "fc" }
: T extends new (...args: any) => Component<infer CProps>
? CProps & { cc?: "cc" }
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T] & { tag?: "tag" }
: { non?: "non" });
const Box = <T extends AsType>(props: PropsWithChildren<AsProps<T>>) => null;
// Test components
class ClassComponent extends Component<{ CCProp: "foo" }> {}
const FunctionComponent: FC<{ FCProp: "foo" }> = () => null;
const Foo: FC = () => (
<>
<Box as={FunctionComponent} fc="fc" FCProp="foo" />
<Box as={ClassComponent} cc="cc" CCProp="foo" />
{/* Here types are inferred incorrectly.*/}
{/* `tag` should be expected */}
{/* `href` should show error that `true` is not assignable to `string` */}
<Box as="a" tag="tag" href />
<Box non="non" />
</>
);
我正在链接 codesandbox,因为如果没有 linting 和自动完成功能很难发现问题。
我正在使用条件类型来期望基于 as
道具中的正确道具,而对于字符串标签,类型系统会直接放弃。
如果您指定标签 as const
它会起作用。如果不这样做,则将其推断为 string
并且 AsProps
的条件部分将其推断为 non
类型。
在四处乱逛时,我发现出于某种原因 f.e。如果您从 AsType
中省略 unknown
,即使没有 as const
,标签也能正常工作。
我想不出正确的解决方案。
您似乎面临的主要问题是编译器使用一些启发式方法来确定是否采用 "a"
之类的值并将其类型推断为 string literal type "a"
, 或者是否将其扩大到 string
。默认情况下,分配给非 const
变量或函数参数的字符串文字值往往会扩大到 string
。
正如您所注意到的,防止这种扩大的一种方法是使用 const
assertion。 "a" as const
的类型将始终为 "a"
,不会扩展为 string
。这在你的情况下有效,但会给 Box
的 caller 带来负担,你可能希望这种情况发生而不要求使用 Box
的人写 as const
任何地方。
另一种防止扩大的方法是在泛型类型推断站点中使用 constrained to a type containing string
or a string literal. (You can read more about the particular rules in the pull request implementing this behavior, microsoft/TypeScript#10676 的值。)所以如果你有 declare function foo<T extends string>(x: T): void;
并调用 foo("a")
,那么T
将被推断为 "a"
。 但是如果你有bar<T extends unknown>(x: T): void;
并调用bar("a")
,那么T
将被推断为string
。包含可分配给 string
的东西的联合类型将起作用,因此 T extends string | number
仍然会给你 "a"
。
也许你现在在想 "well, keyof JSX.IntrinsicElements | unknown
should be a union containing something assignable to string
, since keyof JSX.IntrinsicElements
looks like "a" | "abbr" | "address" | "area" | "article" | ...
and each one of those is a string literal"。好吧,正如您也注意到的那样,unknown
确实把事情搞砸了。你看,unknown
is TypeScript's top type. A top type 是一个 通用超类型 ;任何类型 X
都将是 unknown
的子类型。因此,X | unknown
将等同于 unknown
。编译器积极地将任何与 unknown
的并集简化为 unknown
。你失去了 "a" | "abbr" | ...
... 它不再提示编译器阻止 "a"
扩大到 string
。
因此,这里的解决方法可能是放弃显式 unknown
,而是使用在可分配的类型方面与其等效但编译器保留为联合而不是的东西积极简化。在引入 unknown
之前,您必须使用 {} | undefined | null
之类的东西来表示 "all possible types",并且仍然有效:
type Unknown = {} | undefined | null;
type AsType =
| keyof JSX.IntrinsicElements
| Unknown
然后你会得到你想要的行为:
<Box as="a" tag="tag" href /> // error!
// ~~~~ <-- true is not a string
好的,希望对你有帮助;祝你好运!
我在尝试正确键入 as
prop from emotion.
import React, { Component, FC, PropsWithChildren } from "react";
// Possible types for `as` prop are tag name or component
type AsType =
| keyof JSX.IntrinsicElements
| unknown
// Infered props based on `as`
type AsProps<T extends AsType> = {
as?: T;
} & (T extends FC<infer FProps>
? FProps & { fc?: "fc" }
: T extends new (...args: any) => Component<infer CProps>
? CProps & { cc?: "cc" }
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T] & { tag?: "tag" }
: { non?: "non" });
const Box = <T extends AsType>(props: PropsWithChildren<AsProps<T>>) => null;
// Test components
class ClassComponent extends Component<{ CCProp: "foo" }> {}
const FunctionComponent: FC<{ FCProp: "foo" }> = () => null;
const Foo: FC = () => (
<>
<Box as={FunctionComponent} fc="fc" FCProp="foo" />
<Box as={ClassComponent} cc="cc" CCProp="foo" />
{/* Here types are inferred incorrectly.*/}
{/* `tag` should be expected */}
{/* `href` should show error that `true` is not assignable to `string` */}
<Box as="a" tag="tag" href />
<Box non="non" />
</>
);
我正在链接 codesandbox,因为如果没有 linting 和自动完成功能很难发现问题。
我正在使用条件类型来期望基于 as
道具中的正确道具,而对于字符串标签,类型系统会直接放弃。
如果您指定标签 as const
它会起作用。如果不这样做,则将其推断为 string
并且 AsProps
的条件部分将其推断为 non
类型。
在四处乱逛时,我发现出于某种原因 f.e。如果您从 AsType
中省略 unknown
,即使没有 as const
,标签也能正常工作。
我想不出正确的解决方案。
您似乎面临的主要问题是编译器使用一些启发式方法来确定是否采用 "a"
之类的值并将其类型推断为 string literal type "a"
, 或者是否将其扩大到 string
。默认情况下,分配给非 const
变量或函数参数的字符串文字值往往会扩大到 string
。
正如您所注意到的,防止这种扩大的一种方法是使用 const
assertion。 "a" as const
的类型将始终为 "a"
,不会扩展为 string
。这在你的情况下有效,但会给 Box
的 caller 带来负担,你可能希望这种情况发生而不要求使用 Box
的人写 as const
任何地方。
另一种防止扩大的方法是在泛型类型推断站点中使用 constrained to a type containing string
or a string literal. (You can read more about the particular rules in the pull request implementing this behavior, microsoft/TypeScript#10676 的值。)所以如果你有 declare function foo<T extends string>(x: T): void;
并调用 foo("a")
,那么T
将被推断为 "a"
。 但是如果你有bar<T extends unknown>(x: T): void;
并调用bar("a")
,那么T
将被推断为string
。包含可分配给 string
的东西的联合类型将起作用,因此 T extends string | number
仍然会给你 "a"
。
也许你现在在想 "well, keyof JSX.IntrinsicElements | unknown
should be a union containing something assignable to string
, since keyof JSX.IntrinsicElements
looks like "a" | "abbr" | "address" | "area" | "article" | ...
and each one of those is a string literal"。好吧,正如您也注意到的那样,unknown
确实把事情搞砸了。你看,unknown
is TypeScript's top type. A top type 是一个 通用超类型 ;任何类型 X
都将是 unknown
的子类型。因此,X | unknown
将等同于 unknown
。编译器积极地将任何与 unknown
的并集简化为 unknown
。你失去了 "a" | "abbr" | ...
... 它不再提示编译器阻止 "a"
扩大到 string
。
因此,这里的解决方法可能是放弃显式 unknown
,而是使用在可分配的类型方面与其等效但编译器保留为联合而不是的东西积极简化。在引入 unknown
之前,您必须使用 {} | undefined | null
之类的东西来表示 "all possible types",并且仍然有效:
type Unknown = {} | undefined | null;
type AsType =
| keyof JSX.IntrinsicElements
| Unknown
然后你会得到你想要的行为:
<Box as="a" tag="tag" href /> // error!
// ~~~~ <-- true is not a string
好的,希望对你有帮助;祝你好运!