【问题标题】:How to create interface for a function which creates a nested element structure in typescript?如何为在打字稿中创建嵌套元素结构的函数创建接口?
【发布时间】:2019-12-21 15:05:38
【问题描述】:

我是打字稿的新手。我正在使用 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;
}

它在stylechildren 上显示错误

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

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

element[prop] = rest[prop]; 行还有一个错误,element.style[prop] = style[prop]; 上还有一个错误

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

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

【问题讨论】:

  • 任何其他键都可以添加到这个对象你是什么意思?你不能在csProps 接口中拥有[key : string]: string; 以及其他属性
  • @Plochie 像tag 这样的一些键是必不可少的。 childrenstyle 是可选的。也可以添加任何其他键。
  • 当您在csProps 中说[key : string]: string; 时,这意味着接口属性键应该是字符串,值应该是字符串,但这是stylechildren 属性的冲突,因为它们不是字符串类型。我希望我说得通。你的问题是[key : string]: string; 声明。
  • @Plochie 谢谢。我得到了它。使用[key : string]: any; 可以消除错误。第二个错误呢?
  • 可以编辑问题以便我发布答案吗?

标签: javascript typescript


【解决方案1】:

回答您的问题

索引属性的可分配性

是的,接口不允许您同时定义字符串索引属性和使用不同定义的特定字符串的属性。您可以使用intersection type 解决此问题:

type ceProps =
    & {
        tag: string;
        style?: Style;
        children?: ceProps[];
    }
    & {
        [key: string]: string;
    };

这告诉 Typescript tag 将永远存在并且永远是一个字符串,style 可能存在也可能不存在,但当它存在时将是 Stylechildren 可能存在也可能不存在在那里,但当它在那里时将是一个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

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

type ceProps =
    & {
        tag: string;
        style?: CSSStyleDeclaration;
        children?: ceProps[];
    }
    & {
        [key: string]: string;
    };

当你这样做时,你不再需要使用 (element.style as any) 来投射 element.style——你可以使用这个:

    //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&lt;number&gt; 的东西将是一个错误。 = string 部分是默认参数——如果我们写成不带尖括号的CeProps,我们的意思是CeProps&lt;string&gt;,即任何字符串。

这样做的好处是 Typescript 支持string literal types,它扩展了字符串。所以你可以使用CeProps&lt;"a"&gt;,然后我们就会知道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&lt;"a"&gt; 值。 (实际上,从技术上讲,它会将其理解为 { tag: "a"; } 类型,但它与 CeProps&lt;"a"&gt; 兼容,并且可以传递给期望的函数,例如。)

最后,如果您有兴趣编写一个函数,该函数只能将CeProps 用于特定标签,而不仅仅是one 标签,您可以使用union type,它用a 表示|:

function takesBoldOrItalics(props: CeProps<"b" | "i">): void {

您可以使用const aBold: CeProps&lt;"b"&gt; = { 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 复制了这个并添加了一些换行符以提高可读性。)

请注意此处的&lt;K extends keyof HTMLElementTagNameMap&gt; 部分。与CeProps 上的&lt;Tag extends string&gt; 一样,这表示带有约束的类型参数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;
    };

现在如果我们写 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&lt;Omit&lt;HTMLElementTagNameMap[Tag], keyof MinimalCeProps&lt;Tag&gt;&gt;&gt; 的交集。这是一口,但我们稍后会分解它。

那么,我们与PartialOmit 有业务往来。要分解它,

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

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

    在我们的例子中,Omit&lt;HTMLElementTagNameMap[Tag], keyof MinimalCeProps&lt;Tag&gt;&gt;,我们忽略了HTMLElementTagNameMap[Tag] 中已经是MinimalCeProps&lt;Tag&gt; 中的属性的属性,即tagstylechildren。这很重要,因为HTMLElementTagNameMap[Tag] 将有一些children 属性——它不会是CeProps[]。我们可以只使用Omit&lt;HTMLElementTagNameMap[Tag], "children"&gt;,但我认为最好彻底——我们希望MinimalCeProps 能够“赢得”所有这些标签。

  • Partial 表示所有传递的类型的属性都应该是可选的。所以Partial&lt;{ foo: number; bar: string; baz: 42[]; }&gt; 将是{ 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;
}

这里,element 自动获取正确的类型,HTMLElementTagNameMap[T] 感谢createElement 的类型声明。然后我们必须创建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" | ...

【讨论】:

  • 您的ElementByTag 定义是否比type ElementByTag&lt;T extends string&gt; = T extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[T] : HTMLElement; 更有优势?每当您有一堆字符串文字并且您使用嵌套条件类型检查它们时,您可能会重构为以这些文字作为键的某个接口/类型的查找(例如,K extends "foo" ? Foo : K extends "bar" ? Bar : ... 变为 K extends keyof KeyMap ? KeyMap[K] : ..interface KeyMap {foo: Foo, bar: Bar, ...} .
  • @jcalz 可能不,我只是不知道HTMLElementTagNameMap。有机会我会调查的。
  • @jcalz 原来我们的项目附加了一个非常过时的lib.dom.d.ts 文件。与我们并没有真正相关(我们不使用本机函数操作 DOM ......曾经吗?),但显然与这个答案混淆了。无论出于何种原因,条件逻辑是我对这种情况的第一个想法——考虑到递归问题,显然它应该是映射。无论如何,更新了一个大大改进的版本,谢谢。
猜你喜欢
  • 2019-06-21
  • 2019-08-19
  • 2016-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-28
  • 2020-11-04
相关资源
最近更新 更多