gpt4 book ai didi

javascript - 如何为在 typescript 中创建嵌套元素结构的函数创建接口(interface)?

转载 作者:行者123 更新时间:2023-12-03 16:49:26 26 4
gpt4 key购买 nike

我是 typescript 新手。我正在使用javascript实现以前创建的功能。该函数接收一个对象。具有以下特性

  • tag:将是string
  • children:这是同一接口(interface)的数组(即ceProps如下所示);
  • style:这将是包含样式的对象,例如(colorfontSize等)
  • 可以将任何其他键添加到该对象。(例如innerHTMLsrc等)

  • 这是通过代码。
    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;
    }

    它显示 stylechildren上的错误

    Property 'style' of type 'Style | undefined' is not assignable to string index type 'string'.ts(2411)

    Property 'children' of type 'ceProps[] | undefined' is not assignable to string index type 'string'.ts(2411)



    element[prop] = rest[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'. No index signature with a parameter of type 'string' was found on type 'HTMLElement'



    请解释每个问题及其解决方法。

    最佳答案

    回答你的问题
    具有索引属性的可分配性
    是的,接口(interface)不允许您同时定义字符串索引属性和使用以不同方式定义的特定字符串的属性。您可以使用intersection type解决此问题:

    type ceProps =
    & {
    tag: string;
    style?: Style;
    children?: ceProps[];
    }
    & {
    [key: string]: string;
    };
    这告诉Typescript 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可以做得更好。 CSSStyleDeclarationTypescript随附的 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知道您的 styleelement.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支持 string literal types,它扩展了字符串。因此,您可以使用 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而不是仅对一个标签采用 |的函数感兴趣,则可以使用 union type,它用 const aBold: CeProps<"b"> = { tag: "b" };表示:
    function takesBoldOrItalics(props: CeProps<"b" | "i">): void {
    您可以使用 const anItalic = { tag: "i" } as const;takesBoldOrItalics({ tag: "b" });调用此函数,也可以像 { tag: "a" }一样直接调用它。但是,如果您尝试使用 keyof HTMLElementTagNameMap调用它,则会收到错误消息。
    将事物限制为 lib.dom.d.ts HTMLElementTagNameMap中的另一个强大工具是 HTMLElement,它为每个可能的HTML标签字符串提供了特定的 lib.dom.d.ts。看起来像这样:
    interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;
    "address": HTMLElement;
    "applet": HTMLAppletElement;
    "area": HTMLAreaElement;
    // ...
    }
    (从 lib.dom.d.ts复制) createElement使用它来键入 lib.dom.d.ts本身,例如:
    createElement<K extends keyof HTMLElementTagNameMap>(
    tagName: K,
    options?: ElementCreationOptions,
    ): HTMLElementTagNameMap[K];
    (我从 <K extends keyof HTMLElementTagNameMap>复制了此代码,并添加了一些换行符以提高可读性。)
    注意这里的 <Tag extends string>部分。与 CeProps上的 K一样,这表示带有约束的类型参数 K。因此, keyof HTMLElementTagNameMap必须是某种 keyof。如果您不熟悉, keyof { foo: number; bar: number; }表示某种类型的“键”(属性名称)。因此 "foo" | "bar"keyof HTMLElementTagNameMap"a" | "abbr" | "address" | "applet" | "area" | ...lib.dom.d.ts-所有潜在HTML标记名称的联合(至少在 createElement的最后一次更新中)。这意味着 tag要求 HTMLElement成为这些字符串之一(它还有其他重载,可以处理其他字符串并仅返回 CeProps)。
    我们可以在 ce({ tag: "image" })中利用相同的功能:
    type CeProps<Tag extends keyof HTMLElementTagNameMap = keyof HTMLElementTagNameMap> =
    & {
    tag: Tag;
    style?: CSSStyleDeclaration;
    children?: CeProps[];
    }
    & {
    [key: string]: string;
    };
    现在,如果我们编写 ce({ tag: "img" })而不是 Tag extends keyof HTMLElementTagNameMap,我们将得到一个错误,而不是被静默接受,然后无法正常工作。
    正确键入其余部分
    如果我们使用 ce,我们可以更精确地键入“rest”属性,这可以防止您犯错误,并且可以限制您需要在 CeProps内部进行的强制类型转换。
    要使用它,我已经像这样更新了 MinimalCeProps:
    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>>>;
    我将其分为两部分,一部分是 CeProps,一部分是您想一直出现的部分,另一部分是完整的 Partial<Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>>,它与 Partial相交。那是一个大嘴巴,但是我们稍后会分解。
    现在,我们通过 OmitHTMLElementTagNameMap[Tag]进行业务。要分解,
  • Tag是与createElement对应的HTML元素。您会注意到,该类型与Omit的返回类型相同。
  • Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">表示我们遗漏了作为第一个参数传入的某些类型的属性,如第二个字符串文字的并集所示。例如,{ bar: 42[]; }将产生Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>
    在我们的示例HTMLElementTagNameMap[Tag]中,我们从MinimalCeProps<Tag>中省略了已经是tag中的属性的属性,即stylechildrenHTMLElementTagNameMap[Tag]。这很重要,因为children将具有CeProps[]属性,而不会是Omit<HTMLElementTagNameMap[Tag], "children">。我们可以只使用MinimalCeProps,但我认为最好是透彻了解-我们希望Partial为所有这些标签“赢”。
  • Partial<{ foo: number; bar: string; baz: 42[]; }>表示所有传递的类型的属性都应设为可选。因此{ foo?: number; bar?: string; baz?: 42[]; }将是CeProps
    在我们的案例中,这只是表明我们不会在这里传递任何HTML元素的每个属性-只是我们有兴趣覆盖的属性。

  • 用这种方式做事有两个好处。首先,这可以防止将拼写错误或类型错误的属性添加到 ce中。其次, element本身可以利用它来减少对转换的依赖:
    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;
    }
    在这里,由于 HTMLElementTagNameMap[T]的类型声明,因此 createElement自动获取正确的类型 otherProps。然后,我们必须创建 any“虚拟变量”,可悲的是,这需要进行一些强制转换-但是我们比强制转换为 !更安全。我们还需要在 otherProps[prop]上使用 !- undefined告诉Typescript该值不是 CeProps。这是因为您可以使用明确的 undefined值创建 { class: undefined },例如 for (const props in otherProps)。由于这是一个奇怪的错误,因此似乎不值得一试。您只需保留的属性就不会有问题,因为它们不会出现在 ce中。
    更重要的是,正确键入了 createElement的返回类型,就像键入 ce({ tag: "a" })一样。这意味着,如果您执行 HTMLAnchorElement,Typescript就会知道您正在获得ojit_code。
    结论:一些示例/测试案例
    // 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" | ...

    关于javascript - 如何为在 typescript 中创建嵌套元素结构的函数创建接口(interface)?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59437256/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com