![](/img/trans.png)
[英]Typescript how to create a nested interface objects using mapped types?
[英]How to create interface for a function which creates a nested element structure in typescript?
我是打字稿的新手。 我正在使用 javascript 实现以前创建的函数。 该函数接收一个对象。 具有以下属性
tag
:这将是一个string
。children
: 这是同一个接口的数组(即如下所示的ceProps
);style
: 这将是包含样式的对象,如( color
、 fontSize
等)innerHTML
、 src
等)这是通过代码。
interface Style {
[key : string ]: string;
}
interface ceProps {
tag: string;
style?: Style;
children?: ceProps[];
[key : string]: string;
}
const ce = ({tag, children, style, ...rest } : ceProps) => {
const element = document.createElement(tag);
//Adding properties
for(let prop in rest){
element[prop] = rest[prop];
}
//Adding children
if(children){
for(let child of children){
element.appendChild(ce(child))
}
}
//Adding styles
if(style){
for(let prop in style){
element.style[prop] = style[prop];
}
}
return element;
}
它在style
和children
上显示错误
'Style | undefined'
类型'Style | undefined'
属性'style'
'Style | undefined'
不能分配给字符串索引类型'string'
.ts(2411)
'ceProps[] | undefined'
类型'ceProps[] | undefined'
属性'children'
'ceProps[] | undefined'
'ceProps[] | undefined'
不能分配给字符串索引类型'string'
.ts(2411)
还有一个错误,一行element[prop] = rest[prop];
和element.style[prop] = style[prop];
上的相同错误element.style[prop] = style[prop];
元素隐式具有 'any' 类型,因为类型 'string' 的表达式不能用于索引类型 'HTMLElement'。 在“HTMLElement”类型上找不到带有“string”类型参数的索引签名
请解释每个问题及其修复方法。
是的,接口不会让您同时定义字符串索引属性和使用不同定义的特定字符串的属性。 您可以使用交叉类型来解决这个问题:
type ceProps =
& {
tag: string;
style?: Style;
children?: ceProps[];
}
& {
[key: string]: string;
};
这告诉打字稿tag
将始终存在并且始终是字符串, style
可能存在也可能不存在,但是当它存在时将是一个Style
,并且children
可能存在或可能不存在,但将是ceProps[]
时它在那里。 任何其他属性也可能存在,并且始终是字符串。
HTMLElement
问题是您指定ceProps
可以包含任何字符串作为属性,但HTMLElement
没有任何字符串作为属性,它为它定义了特定的属性。
您可以通过将element
为any
或element.style
为any
来逃避 Typescript 的检查,如下所示:
//Adding properties
for (const prop in rest) {
(element as any)[prop] = rest[prop];
}
if (style) {
for (const prop in style) {
(element.style as any)[prop] = style[prop];
}
}
但是,这不是类型安全的。 没有检查ceProps
中的属性实际上是您创建的元素可以拥有或使用的属性。 HTML 是非常宽容的——大多数时候该属性会被默默地忽略——但这可能比崩溃更令人费解,因为你不会有任何迹象表明出了什么问题。
通常,您应该非常谨慎地使用any
. 有时你必须这样做,但它应该总是让你不舒服。
这将使您可以将现有代码编译为 Typescript,并且至少会提供一点类型安全性。 不过,Typescript 可以做得更好。
CSSStyleDeclaration
lib.dom.d.ts
附带的lib.dom.d.ts
文件包含大量 HTML 和原生 Javascript 中各种事物的定义。 其中之一是CSSStyleDeclaration
,一种用于样式化 HTML 元素的类型。 使用它而不是您自己的Style
声明:
type ceProps =
& {
tag: string;
style?: CSSStyleDeclaration;
children?: ceProps[];
}
& {
[key: string]: string;
};
当你这样做,你不再需要转换element.style
与(element.style as any)
-你可以只使用这样的:
//Adding styles
if (style) {
for (const prop in style) {
element.style[prop] = style[prop];
}
}
这是有效的,因为现在 Typescript 知道您的style
与element.style
是同一类型的对象,因此这将正确运行。 作为奖励,现在当你首先创建你的ceProps
时,如果你使用了一个错误的属性——双赢,你会得到一个错误。
ceProps
的定义将允许您定义一个结构,该结构将与ce
一起创建任何元素。 但是这里一个可能更好的解决方案是使其通用。 这样我们就可以跟踪哪个标签与ceProps
的特定实例相关联。
type CeProps<Tag extends string = string> =
& {
tag: Tag;
style?: CSSStyleDeclaration;
children?: CeProps[];
}
& {
[key: string]: string;
};
(我将ceProps
重命名为CeProps
以更符合典型的 Typescript 命名风格,当然欢迎您的项目使用自己的风格。)
尖括号表示泛型类型参数,这里是Tag
。 让Tag extends string
意味着Tag
被限制为一个字符串——像CeProps<number>
这样的东西将是一个错误。 = string
部分是默认参数——如果我们写的CeProps
没有尖括号,我们的意思是CeProps<string>
,即任何字符串。
这样做的好处是 Typescript 支持字符串文字类型,它扩展了字符串。 所以你可以使用CeProps<"a">
,然后我们就会知道tag
不仅仅是任何字符串,而是"a"
。
那么我们就有能力指出我们正在谈论的标签。 例如:
const props: CeProps<"a"> = { tag: "a", href: "test" };
如果你在这里写tag: "b"
,你会得到一个错误——Typescript 将要求这是一个"a"
。 您可以编写一个只需要特定CeProps
的函数,等等。
如果您使用as const
关键字,Typescript 也可以正确推断:
const props = { tag: "a" } as const;
Typescript 会理解这个props
变量是一个CeProps<"a">
值。 (实际上,从技术上讲,它会将其理解为{ tag: "a"; }
类型,但它与CeProps<"a">
兼容,并且可以传递给期望它的函数,例如。)
最后,如果你有兴趣编写一个只能为特定标签使用CeProps
的函数,而不仅仅是一个标签,你可以使用联合类型,用|
表示|
:
function takesBoldOrItalics(props: CeProps<"b" | "i">): void {
你可以用const aBold: CeProps<"b"> = { tag: "b" };
调用这个函数const aBold: CeProps<"b"> = { tag: "b" };
, 或使用const anItalic = { tag: "i" } as const;
, 或者直接调用它,比如takesBoldOrItalics({ tag: "b" });
. 但是如果你尝试用{ tag: "a" }
调用它,你会得到一个错误。
keyof HTMLElementTagNameMap
lib.dom.d.ts
另一个强大的工具是HTMLElementTagNameMap
,它为每个可能的 HTML 标记字符串提供特定的HTMLElement
。 它看起来像这样:
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
// ...
}
(复制自lib.dom.d.ts
)
这被lib.dom.d.ts
用来输入createElement
本身,例如:
createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
options?: ElementCreationOptions,
): HTMLElementTagNameMap[K];
(我从lib.dom.d.ts
复制了这个并添加了一些换行符以提高可读性。)
注意这里的<K extends keyof HTMLElementTagNameMap>
部分。 与我们在CeProps
上的<Tag extends string>
CeProps
,这表示带有约束的类型参数K
所以K
必须是keyof HTMLElementTagNameMap
某种keyof HTMLElementTagNameMap
。 如果您不熟悉, keyof
表示某种类型的“键”——属性名称。 所以keyof { foo: number; bar: number; }
keyof { foo: number; bar: number; }
keyof { foo: number; bar: number; }
是"foo" | "bar"
"foo" | "bar"
。 而keyof HTMLElementTagNameMap
是"a" | "abbr" | "address" | "applet" | "area" | ...
"a" | "abbr" | "address" | "applet" | "area" | ...
"a" | "abbr" | "address" | "applet" | "area" | ...
—所有潜在 HTML 标记名称的联合(至少截至上次更新lib.dom.d.ts
)。 这意味着createElement
要求tag
是这些字符串之一(它还有其他重载处理其他字符串并只返回一个HTMLElement
)。
我们可以在我们的CeProps
利用同样的功能:
type CeProps<Tag extends keyof HTMLElementTagNameMap = keyof HTMLElementTagNameMap> =
& {
tag: Tag;
style?: CSSStyleDeclaration;
children?: CeProps[];
}
& {
[key: string]: string;
};
现在如果我们写ce({ tag: "image" })
而不是ce({ tag: "img" })
我们会得到一个错误而不是它被默默接受然后不能正常工作。
如果我们使用Tag extends keyof HTMLElementTagNameMap
,我们可以更精确地键入“rest”属性,这可以防止您犯错误并限制您需要在ce
执行的转换量。
为了使用它,我像这样更新了CeProps
:
interface MinimalCeProps<Tag extends keyof HTMLElementTagNameMap> {
tag: Tag;
style?: CSSStyleDeclaration;
children?: CeProps[];
}
type CeProps<Tag extends keyof HTMLElementTagNameMap = keyof HTMLElementTagNameMap> =
& MinimalCeProps<Tag>
& Partial<Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>>;
我将它分成两部分, MinimalCeProps
用于您希望始终出现的部分,然后是完整的CeProps
,它生成该类型与Partial<Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>>
。 这是一口,但我们稍后会分解它。
现在,我们有Partial
和Omit
业务。 要分解它,
HTMLElementTagNameMap[Tag]
是Tag
对应的 HTML 元素。 您会注意到这与createElement
上的返回类型使用的类型相同。
Omit
表示我们Omit
了作为第一个参数传入的类型的一些属性,如第二个中字符串文字的并集所示。 例如, Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">
Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">
Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">
将导致{ bar: 42[]; }
{ bar: 42[]; }
.
在我们的例子中, Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>
,我们Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>
了HTMLElementTagNameMap[Tag]
中已经是MinimalCeProps<Tag>
属性的属性——即tag
、 style
和children
。 这很重要,因为HTMLElementTagNameMap[Tag]
将有一些children
属性——它不会是CeProps[]
。 我们可以只使用Omit<HTMLElementTagNameMap[Tag], "children">
但我认为最好是彻底的——我们希望MinimalCeProps
能够“赢得”所有这些标签。
Partial
表示所有传递类型的属性都应该是可选的。 所以Partial<{ foo: number; bar: string; baz: 42[]; }>
Partial<{ foo: number; bar: string; baz: 42[]; }>
Partial<{ foo: number; bar: string; baz: 42[]; }>
将是{ foo?: number; bar?: string; baz?: 42[]; }
{ foo?: number; bar?: string; baz?: 42[]; }
{ foo?: number; bar?: string; baz?: 42[]; }
.
在我们的例子中,这只是为了表明我们不会在这里传递任何 HTML 元素的每个属性——只是我们有兴趣覆盖的那些。
这样做有两个好处。 首先,这可以防止将输入错误或输入错误的属性添加到CeProps
。 其次,它可以被ce
本身利用来减少对铸造的依赖:
function ce<T extends keyof HTMLElementTagNameMap>(
{ tag, children, style, ...rest }: CeProps<T>,
): HTMLElementTagNameMap[T] {
const element = window.document.createElement(tag);
//Adding properties
const otherProps = rest as unknown as Partial<HTMLElementTagNameMap[T]>;
for (const prop in otherProps) {
element[prop] = otherProps[prop]!;
}
//Adding children
if (children) {
for (const child of children) {
element.appendChild(ce(child));
}
}
//Adding styles
if (style) {
for (const prop in style) {
element.style[prop] = style[prop];
}
}
return element;
}
在这里,由于createElement
的类型声明, element
自动获得正确的类型HTMLElementTagNameMap[T]
。 然后我们必须创建otherProps
“虚拟变量”,遗憾的是这需要一些转换——但我们可以比转换到any
更安全。 我们还需要使用!
在otherProps[prop]
—— !
告诉 Typescript 该值不是undefined
。 这是因为您可以创建一个带有明确undefined
值的CeProps
,例如{ class: undefined }
。 由于这将是一个奇怪的错误,因此似乎不值得对其进行检查。 您刚刚省略的属性不会有问题,因为它们不会出现在for (const props in otherProps)
。
更重要的是, ce
的返回类型的类型是正确的——就像createElement
的类型一样。 这意味着如果你执行ce({ tag: "a" })
,Typescript 会知道你得到了一个HTMLAnchorElement
。
// Literal
ce({
tag: "a",
href: "test",
}); // HTMLAnchorElement
// Assigned to a variable without as const
const variable = {
tag: "a",
href: "test",
};
ce(variable); // Argument of type '{ tag: string; href: string; }' is not assignable to parameter of type 'CeProps<...
// Assigned to a variable using as const
const asConst = {
tag: "a",
href: "test",
} as const;
ce(asConst); // HTMLAnchorElement
// Giving invalid href property
ce({
tag: "a",
href: 42,
}); // 'number' is not assignable to 'string | undefined'
// Giving invalid property
ce({
tag: "a",
invalid: "foo",
}); // Argument of type '{ tag: "a"; invalid: string; }' is not assignable to parameter of type 'CeProps<"a">'.
// Object literal may only specify known properties, but 'invalid' does not exist in type 'CeProps<"a">'.
// Did you mean to write 'oninvalid'?
// Giving invalid tag
ce({ tag: "foo" }); // Type '"foo"' is not assignable to type '"object" | "link" | "small" | ...
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.