[英]How to create interface for a function which creates a nested element structure in typescript?
I am new to typescript.我是打字稿的新手。 I am implementing a previously created function using javascript.
我正在使用 javascript 实现以前创建的函数。 The function takes in an object.
该函数接收一个对象。 Which have following properties
具有以下属性
tag
: which will be a string
. tag
:这将是一个string
。children
: which is array of the same interface(ie ceProps
as shown below); children
: 这是同一个接口的数组(即如下所示的ceProps
);style
: which will be object containing styles like ( color
, fontSize
etc.) style
: 这将是包含样式的对象,如( color
、 fontSize
等)innerHTML
, src
etc.)innerHTML
、 src
等) Here is by code.这是通过代码。
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;
}
It shows error on the style
and children
它在
style
和children
上显示错误
Property
'style'
of type'Style | undefined'
'Style | undefined'
类型'Style | undefined'
属性'style'
'Style | undefined'
is not assignable to string index type'string'
.ts(2411)'Style | undefined'
不能分配给字符串索引类型'string'
.ts(2411)Property
'children'
of type'ceProps[] | undefined'
'ceProps[] | undefined'
类型'ceProps[] | undefined'
属性'children'
'ceProps[] | undefined'
'ceProps[] | undefined'
is not assignable to string index type'string'
.ts(2411)'ceProps[] | undefined'
不能分配给字符串索引类型'string'
.ts(2411)
There is one more error one the line element[prop] = rest[prop];
还有一个错误,一行
element[prop] = rest[prop];
and same error on element.style[prop] = style[prop];
和
element.style[prop] = style[prop];
上的相同错误element.style[prop] = style[prop];
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'HTMLElement'.
元素隐式具有 'any' 类型,因为类型 'string' 的表达式不能用于索引类型 'HTMLElement'。 No index signature with a parameter of type 'string' was found on type 'HTMLElement'
在“HTMLElement”类型上找不到带有“string”类型参数的索引签名
Kindly explain each problem and its fix.请解释每个问题及其修复方法。
Yes, interfaces will not let you define both a string-index property, and properties using specific strings that are defined differently.是的,接口不会让您同时定义字符串索引属性和使用不同定义的特定字符串的属性。 You can get around this using an intersection type :
您可以使用交叉类型来解决这个问题:
type ceProps =
& {
tag: string;
style?: Style;
children?: ceProps[];
}
& {
[key: string]: string;
};
This tells Typescript that tag
will always be there and always be a string, style
may or may not be there, but will be a Style
when it is there, and children
may or may not be there, but will be a ceProps[]
when it is there.这告诉打字稿
tag
将始终存在并且始终是字符串, style
可能存在也可能不存在,但是当它存在时将是一个Style
,并且children
可能存在或可能不存在,但将是ceProps[]
时它在那里。 Any other property may also exist, and will always be a string.任何其他属性也可能存在,并且始终是字符串。
HTMLElement
HTMLElement
The problem is that you specified that ceProps
could include any string as a property, but HTMLElement
does not have any string ever as a property, it has particular properties defined for it.问题是您指定
ceProps
可以包含任何字符串作为属性,但HTMLElement
没有任何字符串作为属性,它为它定义了特定的属性。
You can escape out of Typescript's checking here by casting element
as any
, or element.style
as any
, like so:您可以通过将
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];
}
}
However, this is not type-safe .但是,这不是类型安全的。 Nothing is checking that the properties in your
ceProps
is actually a property that the element you created can have or use.没有检查
ceProps
中的属性实际上是您创建的元素可以拥有或使用的属性。 HTML is pretty forgiving—most of the time the property will just be silently ignored—but that can be even more hair-pulling than a crash is, because you'll have no indication what's wrong. HTML 是非常宽容的——大多数时候该属性会被默默地忽略——但这可能比崩溃更令人费解,因为你不会有任何迹象表明出了什么问题。
In general, you should be extremely cautious about using any
.通常,您应该非常谨慎地使用
any
. Sometimes you have to, but it should always make you uncomfortable.有时你必须这样做,但它应该总是让你不舒服。
This will let you compile your existing code as Typescript, and it will provide at least a little bit of type-safety.这将使您可以将现有代码编译为 Typescript,并且至少会提供一点类型安全性。 Typescript can do much, much better though.
不过,Typescript 可以做得更好。
CSSStyleDeclaration
The lib.dom.d.ts
file that comes with Typescript has loads of definitions for all kinds of things in HTML and native Javascript. lib.dom.d.ts
附带的lib.dom.d.ts
文件包含大量 HTML 和原生 Javascript 中各种事物的定义。 One of them is CSSStyleDeclaration
, a type used for styling HTML elements.其中之一是
CSSStyleDeclaration
,一种用于样式化 HTML 元素的类型。 Use this instead of your own Style
declaration:使用它而不是您自己的
Style
声明:
type ceProps =
& {
tag: string;
style?: CSSStyleDeclaration;
children?: ceProps[];
}
& {
[key: string]: string;
};
When you do this, you no longer need to cast element.style
with (element.style as any)
—you can just use this:当你这样做,你不再需要转换
element.style
与(element.style as any)
-你可以只使用这样的:
//Adding styles
if (style) {
for (const prop in style) {
element.style[prop] = style[prop];
}
}
This works because now Typescript knows that your style
is the same kind of object as element.style
, so this will work out correctly.这是有效的,因为现在 Typescript 知道您的
style
与element.style
是同一类型的对象,因此这将正确运行。 As a bonus, now when you create your ceProps
in the first place, you'll get an error if you use a bad property—win-win.作为奖励,现在当你首先创建你的
ceProps
时,如果你使用了一个错误的属性——双赢,你会得到一个错误。
The definition of ceProps
will allow you to define a structure that will work with ce
to create any element. ceProps
的定义将允许您定义一个结构,该结构将与ce
一起创建任何元素。 But a potentially better solution here is to make this generic.但是这里一个可能更好的解决方案是使其通用。 That way we can track which tag is associated with a particular instance of
ceProps
.这样我们就可以跟踪哪个标签与
ceProps
的特定实例相关联。
type CeProps<Tag extends string = string> =
& {
tag: Tag;
style?: CSSStyleDeclaration;
children?: CeProps[];
}
& {
[key: string]: string;
};
(I renamed ceProps
to CeProps
to be more in-line with typical Typescript naming style, though of course your project is welcome to use its own style.) (我将
ceProps
重命名为CeProps
以更符合典型的 Typescript 命名风格,当然欢迎您的项目使用自己的风格。)
The angle brackets indicate generic type parameters, here Tag
.尖括号表示泛型类型参数,这里是
Tag
。 Having Tag extends string
means that Tag
is constrained to be a string—something like CeProps<number>
will be an error.让
Tag extends string
意味着Tag
被限制为一个字符串——像CeProps<number>
这样的东西将是一个错误。 The = string
part is a default parameter—if we write CeProps
without angle brackets, we mean CeProps<string>
, that is, any string. = string
部分是默认参数——如果我们写的CeProps
没有尖括号,我们的意思是CeProps<string>
,即任何字符串。
The advantage of this is that Typescript supports string literal types , which extend string.这样做的好处是 Typescript 支持字符串文字类型,它扩展了字符串。 So you could use
CeProps<"a">
, and then we would know that tag
is not just any string, but "a"
specifically.所以你可以使用
CeProps<"a">
,然后我们就会知道tag
不仅仅是任何字符串,而是"a"
。
So then we have the ability to indicate what tag we're talking about.那么我们就有能力指出我们正在谈论的标签。 For example:
例如:
const props: CeProps<"a"> = { tag: "a", href: "test" };
If you were to write tag: "b"
here, you would get an error—Typescript will require that this be an "a"
.如果你在这里写
tag: "b"
,你会得到一个错误——Typescript 将要求这是一个"a"
。 You could write a function that takes only a specific CeProps
maybe, and so on.您可以编写一个只需要特定
CeProps
的函数,等等。
Typescript can also infer this correctly if you use the as const
keyword:如果您使用
as const
关键字,Typescript 也可以正确推断:
const props = { tag: "a" } as const;
Typescript will understand that this props
variable is a CeProps<"a">
value. Typescript 会理解这个
props
变量是一个CeProps<"a">
值。 (Actually, technically, it will understand it as a { tag: "a"; }
type, but that is compatible with CeProps<"a">
and can be passed to a function expecting that, for example.) (实际上,从技术上讲,它会将其理解为
{ tag: "a"; }
类型,但它与CeProps<"a">
兼容,并且可以传递给期望它的函数,例如。)
Finally, if you are interested in writing a function that can only take CeProps
for particular tags, but not just one tag, you can use a union type , which is indicated with a |
最后,如果你有兴趣编写一个只能为特定标签使用
CeProps
的函数,而不仅仅是一个标签,你可以使用联合类型,用|
表示|
: :
function takesBoldOrItalics(props: CeProps<"b" | "i">): void {
You could call this function with const aBold: CeProps<"b"> = { tag: "b" };
你可以用
const aBold: CeProps<"b"> = { tag: "b" };
调用这个函数const aBold: CeProps<"b"> = { tag: "b" };
, or with const anItalic = { tag: "i" } as const;
, 或使用
const anItalic = { tag: "i" } as const;
, or just call it directly like takesBoldOrItalics({ tag: "b" });
, 或者直接调用它,比如
takesBoldOrItalics({ tag: "b" });
. . But if you try to call it with
{ tag: "a" }
you'll get an error.但是如果你尝试用
{ tag: "a" }
调用它,你会得到一个错误。
keyof HTMLElementTagNameMap
keyof HTMLElementTagNameMap
Another powerful tool in lib.dom.d.ts
is HTMLElementTagNameMap
, which gives the specific HTMLElement
for each possible HTML tag string. lib.dom.d.ts
另一个强大的工具是HTMLElementTagNameMap
,它为每个可能的 HTML 标记字符串提供特定的HTMLElement
。 It looks like this:它看起来像这样:
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
// ...
}
(Copied from lib.dom.d.ts
) (复制自
lib.dom.d.ts
)
This is used by lib.dom.d.ts
to type createElement
itself, for example:这被
lib.dom.d.ts
用来输入createElement
本身,例如:
createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
options?: ElementCreationOptions,
): HTMLElementTagNameMap[K];
(I copied this from lib.dom.d.ts
and added some line breaks for readability.) (我从
lib.dom.d.ts
复制了这个并添加了一些换行符以提高可读性。)
Notice the <K extends keyof HTMLElementTagNameMap>
part here.注意这里的
<K extends keyof HTMLElementTagNameMap>
部分。 As with our <Tag extends string>
on CeProps
, this indicates a type parameter K
with a constraint.与我们在
CeProps
上的<Tag extends string>
CeProps
,这表示带有约束的类型参数K
So K
must be some kind of keyof HTMLElementTagNameMap
.所以
K
必须是keyof HTMLElementTagNameMap
某种keyof HTMLElementTagNameMap
。 If you are unfamiliar, keyof
indicates the “keys of” some type—the property names.如果您不熟悉,
keyof
表示某种类型的“键”——属性名称。 So keyof { foo: number; bar: number; }
所以
keyof { foo: number; bar: number; }
keyof { foo: number; bar: number; }
keyof { foo: number; bar: number; }
is "foo" | "bar"
keyof { foo: number; bar: number; }
是"foo" | "bar"
"foo" | "bar"
. "foo" | "bar"
。 And keyof HTMLElementTagNameMap
is "a" | "abbr" | "address" | "applet" | "area" | ...
而
keyof HTMLElementTagNameMap
是"a" | "abbr" | "address" | "applet" | "area" | ...
"a" | "abbr" | "address" | "applet" | "area" | ...
"a" | "abbr" | "address" | "applet" | "area" | ...
—a union of all of the potential HTML tag names (at least as of the last update to lib.dom.d.ts
). "a" | "abbr" | "address" | "applet" | "area" | ...
—所有潜在 HTML 标记名称的联合(至少截至上次更新lib.dom.d.ts
)。 That means createElement
is requiring tag
to be one of those strings (there are other overloads for it that handle other strings and just returns an HTMLElement
).这意味着
createElement
要求tag
是这些字符串之一(它还有其他重载处理其他字符串并只返回一个HTMLElement
)。
We can leverage this same functionality in our CeProps
:我们可以在我们的
CeProps
利用同样的功能:
type CeProps<Tag extends keyof HTMLElementTagNameMap = keyof HTMLElementTagNameMap> =
& {
tag: Tag;
style?: CSSStyleDeclaration;
children?: CeProps[];
}
& {
[key: string]: string;
};
Now if we wrote ce({ tag: "image" })
instead of ce({ tag: "img" })
we would get an error instead of it being silently accepted and then not working correctly.现在如果我们写
ce({ tag: "image" })
而不是ce({ tag: "img" })
我们会得到一个错误而不是它被默默接受然后不能正常工作。
If we use Tag extends keyof HTMLElementTagNameMap
, we can type the “rest” properties more precisely, which protects you from making mistakes as well as limits the amount of casting you need to do inside ce
.如果我们使用
Tag extends keyof HTMLElementTagNameMap
,我们可以更精确地键入“rest”属性,这可以防止您犯错误并限制您需要在ce
执行的转换量。
To use it, I've updated CeProps
like this:为了使用它,我像这样更新了
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>>>;
I split it up into two parts, MinimalCeProps
for the parts you want to always appear, and then the full CeProps
which produces the intersection of that type with Partial<Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>>
.我将它分成两部分,
MinimalCeProps
用于您希望始终出现的部分,然后是完整的CeProps
,它生成该类型与Partial<Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>>
。 That's a mouthful, but we'll break it down in a moment.这是一口,但我们稍后会分解它。
Now then, we have that business with Partial
and Omit
.现在,我们有
Partial
和Omit
业务。 To break it down,要分解它,
HTMLElementTagNameMap[Tag]
is the HTML element corresponding to Tag
. HTMLElementTagNameMap[Tag]
是Tag
对应的 HTML 元素。 You'll notice this is the same type used as the return type on createElement
.您会注意到这与
createElement
上的返回类型使用的类型相同。
Omit
indicates that we are leaving out some properties of the type we pass in as the first parameter, as indicated by the union of string literals in the second. Omit
表示我们Omit
了作为第一个参数传入的类型的一些属性,如第二个中字符串文字的并集所示。 For example, 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">
Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">
will result in { bar: 42[]; }
Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">
将导致{ bar: 42[]; }
{ bar: 42[]; }
. { bar: 42[]; }
.
In our case, Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>
, we are leaving out the properties from HTMLElementTagNameMap[Tag]
that are already properties in MinimalCeProps<Tag>
—namely, tag
, style
, and children
.在我们的例子中,
Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>
,我们Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>
了HTMLElementTagNameMap[Tag]
中已经是MinimalCeProps<Tag>
属性的属性——即tag
、 style
和children
。 This is important because HTMLElementTagNameMap[Tag]
is going to have some children
property—and it's not going to be CeProps[]
.这很重要,因为
HTMLElementTagNameMap[Tag]
将有一些children
属性——它不会是CeProps[]
。 We could just use Omit<HTMLElementTagNameMap[Tag], "children">
but I thought it best to be thorough—we want MinimalCeProps
to “win” for all those tags.我们可以只使用
Omit<HTMLElementTagNameMap[Tag], "children">
但我认为最好是彻底的——我们希望MinimalCeProps
能够“赢得”所有这些标签。
Partial
indicates that all of the passed type's properties should be made optional. Partial
表示所有传递类型的属性都应该是可选的。 So Partial<{ foo: number; bar: string; baz: 42[]; }>
所以
Partial<{ foo: number; bar: string; baz: 42[]; }>
Partial<{ foo: number; bar: string; baz: 42[]; }>
Partial<{ foo: number; bar: string; baz: 42[]; }>
will be { 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[]; }
. { foo?: number; bar?: string; baz?: 42[]; }
.
In our case, this is just to indicate that we aren't going to pass in every property of whatever HTML element here—just the ones we're interested in overriding.在我们的例子中,这只是为了表明我们不会在这里传递任何 HTML 元素的每个属性——只是我们有兴趣覆盖的那些。
There are two advantages to doing things this way.这样做有两个好处。 First of all, this prevents typo'd or mis-typed properties from being added to the
CeProps
.首先,这可以防止将输入错误或输入错误的属性添加到
CeProps
。 Second, it can be leveraged by ce
itself to reduce the reliance on casting:其次,它可以被
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;
}
Here, element
automatically gets the correct type, HTMLElementTagNameMap[T]
thanks to createElement
's type declaration.在这里,由于
createElement
的类型声明, element
自动获得正确的类型HTMLElementTagNameMap[T]
。 Then we have to create the otherProps
“dummy variable,” and sadly that requires some casting—but we can be safer than casting to any
.然后我们必须创建
otherProps
“虚拟变量”,遗憾的是这需要一些转换——但我们可以比转换到any
更安全。 We also need to use !
我们还需要使用
!
on otherProps[prop]
—the !
在
otherProps[prop]
—— !
tells Typescript that the value isn't undefined
.告诉 Typescript 该值不是
undefined
。 This is because you could create a CeProps
with an explicitly undefined
value, like { class: undefined }
.这是因为您可以创建一个带有明确
undefined
值的CeProps
,例如{ class: undefined }
。 Since that would be a weird mistake to make, it doesn't seem worth checking against it.由于这将是一个奇怪的错误,因此似乎不值得对其进行检查。 Properties you just leave out won't be a problem, because they won't appear in
for (const props in otherProps)
.您刚刚省略的属性不会有问题,因为它们不会出现在
for (const props in otherProps)
。
And more importantly, the return type of ce
is correctly typed—just the same way that createElement
is typed.更重要的是,
ce
的返回类型的类型是正确的——就像createElement
的类型一样。 This means that if you do ce({ tag: "a" })
, Typescript will know you're getting an HTMLAnchorElement
.这意味着如果你执行
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.