如何为在打字稿中创建嵌套元素结构的函数创建接口?

Mah*_*Ali 3 javascript typescript

我是打字稿的新手。我正在使用 javascript 实现以前创建的函数。该函数接收一个对象。具有以下属性

  • tag: 这将是一个string.
  • children: 是同一个接口的数组(即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;
}
Run Code Online (Sandbox Code Playgroud)

它显示错误stylechildren

'style'类型的属性'Style | undefined'不可分配给字符串索引类型'string'.ts(2411)

'children'类型的属性'ceProps[] | undefined'不可分配给字符串索引类型'string'.ts(2411)

线上element[prop] = rest[prop];还有一个错误,同样的错误element.style[prop] = style[prop];

元素隐式具有 'any' 类型,因为类型 'string' 的表达式不能用于索引类型 'HTMLElement'。在“HTMLElement”类型上找不到带有“string”类型参数的索引签名

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

KRy*_*yan 8

回答您的问题

索引属性的可分配性

是的,接口不会让您同时定义字符串索引属性和使用不同定义的特定字符串的属性。您可以使用交叉类型来解决这个问题:

type ceProps =
    & {
        tag: string;
        style?: Style;
        children?: ceProps[];
    }
    & {
        [key: string]: string;
    };
Run Code Online (Sandbox Code Playgroud)

这告诉 Typescripttag将始终存在并且始终是一个字符串,style可能存在也可能不存在,但是在存在Style时会是 a ,children可能存在也可能不存在,但是在存在ceProps[]时会是 a 。任何其他属性也可能存在,并且始终是字符串。

索引 HTMLElement

问题是您指定ceProps可以包含任何字符串作为属性,但HTMLElement没有任何字符串作为属性,它具有为其定义的特定属性。

您可以通过强制转换element为 asanyelement.styleas来逃避 Typescript 的检查any,如下所示:

    //Adding properties
    for (const prop in rest) {
        (element as any)[prop] = rest[prop];
    }
Run Code Online (Sandbox Code Playgroud)
    if (style) {
        for (const prop in style) {
            (element.style as any)[prop] = style[prop];
        }
    }
Run Code Online (Sandbox Code Playgroud)

但是,这不是类型安全的。没有什么是检查您的属性ceProps实际上是您创建的元素可以拥有或使用的属性。HTML 是非常宽容的——大多数时候该属性会被默默地忽略——但这可能比崩溃更令人费解,因为你不会有任何迹象表明出了什么问题。

通常,您应该非常谨慎地使用any. 有时你必须这样做,但它应该总是让你不舒服。

提高类型安全性

这将使您可以将现有代码编译为 Typescript,并且至少会提供一点类型安全性。不过,Typescript 可以做得更好。

CSSStyleDeclaration

lib.dom.d.tsTypescript 附带的文件包含大量关于 HTML 和原生 Javascript 中各种事物的定义。其中之一是CSSStyleDeclaration,一种用于样式化 HTML 元素的类型。使用它而不是您自己的Style声明:

type ceProps =
    & {
        tag: string;
        style?: CSSStyleDeclaration;
        children?: ceProps[];
    }
    & {
        [key: string]: string;
    };
Run Code Online (Sandbox Code Playgroud)

当你这样做,你不再需要投element.style(element.style as any)-你可以只使用这样的:

    //Adding styles
    if (style) {
        for (const prop in style) {
            element.style[prop] = style[prop];
        }
    }
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为现在 Typescript 知道您style的对象与 相同element.style,因此这将正确运行。作为奖励,现在当你首先创建你的ceProps时,如果你使用了一个错误的属性——双赢,你会得到一个错误。

通用类型

的定义ceProps将允许您定义可ce用于创建任何元素的结构。但是这里一个可能更好的解决方案是使其通用。这样我们就可以跟踪哪个标签与ceProps.

type CeProps<Tag extends string = string> =
    & {
        tag: Tag;
        style?: CSSStyleDeclaration;
        children?: CeProps[];
    }
    & {
        [key: string]: string;
    };
Run Code Online (Sandbox Code Playgroud)

(我重命名cePropsCeProps更符合典型的 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" };
Run Code Online (Sandbox Code Playgroud)

如果你在tag: "b"这里写,你会得到一个错误——打字稿将要求这是一个"a". 您可以编写一个函数,它只需要一个特定的CeProps可能,依此类推。

如果您使用as const关键字,Typescript 也可以正确推断:

const props = { tag: "a" } as const;
Run Code Online (Sandbox Code Playgroud)

Typescript 会理解这个props变量是一个CeProps<"a">值。(实际上,从技术上讲,它会将其理解为一种{ tag: "a"; }类型,但例如,它与CeProps<"a">期望该类型的函数兼容并可传递给该函数。)

最后,如果您有兴趣编写一个只能CeProps用于特定标签的函数,而不仅仅是一个标签,您可以使用联合类型,用 表示|

function takesBoldOrItalics(props: CeProps<"b" | "i">): void {
Run Code Online (Sandbox Code Playgroud)

您可以使用const aBold: CeProps<"b"> = { tag: "b" };或 with调用此函数,或者const anItalic = { tag: "i" } as const;直接像takesBoldOrItalics({ tag: "b" });. 但是如果你尝试调用它,{ tag: "a" }你会得到一个错误。

约束事物 keyof HTMLElementTagNameMap

中另一个强大的工具lib.dom.d.tsHTMLElementTagNameMap,它给出了HTMLElement每个可能的 HTML 标记字符串的具体信息。它看起来像这样:

interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;
    "address": HTMLElement;
    "applet": HTMLAppletElement;
    "area": HTMLAreaElement;
    // ...
}
Run Code Online (Sandbox Code Playgroud)

(复制自lib.dom.d.ts)

这用于lib.dom.d.ts键入createElement本身,例如:

createElement<K extends keyof HTMLElementTagNameMap>(
    tagName: K,
    options?: ElementCreationOptions,
): HTMLElementTagNameMap[K];
Run Code Online (Sandbox Code Playgroud)

(我从这里复制lib.dom.d.ts并添加了一些换行符以提高可读性。)

注意<K extends keyof HTMLElementTagNameMap>这里的部分。与我们的<Tag extends string>on 一样CeProps,这表示K带有约束的类型参数。所以K一定是某种keyof HTMLElementTagNameMap。如果您不熟悉,请keyof指出某种类型的“键”——属性名称。所以keyof { foo: number; bar: number; }"foo" | "bar"。并且keyof HTMLElementTagNameMap"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;
    };
Run Code Online (Sandbox Code Playgroud)

现在如果我们写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>>>;
Run Code Online (Sandbox Code Playgroud)

我将它分成两部分,MinimalCeProps对于您希望始终出现的部分,然后是CeProps产生该类型与Partial<Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>>. 这是一口,但我们稍后会分解它。

那么现在,我们有生意PartialOmit。要分解它,

  • HTMLElementTagNameMap[Tag]是对应于 的 HTML 元素Tag。您会注意到这与 上使用的返回类型相同createElement

  • Omit表示我们忽略了作为第一个参数传入的类型的一些属性,如第二个中字符串文字的并集所示。例如,Omit<{ foo: string; bar: number; baz: 42[]; }, "foo" | "bar">将导致{ bar: 42[]; }.

    在我们的例子中,Omit<HTMLElementTagNameMap[Tag], keyof MinimalCeProps<Tag>>我们就放弃了从属性HTMLElementTagNameMap[Tag]中已有的性质MinimalCeProps<Tag>-namely, ,tagstylechildren。这很重要,因为HTMLElementTagNameMap[Tag]将有一些children属性——它不会是CeProps[]。我们可以直接使用,Omit<HTMLElementTagNameMap[Tag], "children">但我认为最好是彻底的——我们希望MinimalCeProps为所有这些标签“取胜”。

  • Partial表示所有传递的类型的属性都应该是可选的。所以Partial<{ 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;
}
Run Code Online (Sandbox Code Playgroud)

在这里,由于的类型声明,element自动获取正确的类型。然后我们必须创建“虚拟变量”,遗憾的是这需要一些转换——但我们可以比转换为更安全。我们还需要使用on —告诉 Typescript 值不是。这是因为您可以创建具有显式值的 ,例如。由于这将是一个奇怪的错误,因此似乎不值得对其进行检查。您刚刚省略的属性不会有问题,因为它们不会出现在.HTMLElementTagNameMap[T]createElementotherPropsany!otherProps[prop]!undefinedCePropsundefined{ 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" | ...
Run Code Online (Sandbox Code Playgroud)