繁体   English   中英

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

[英]How to create interface for a function which creates a nested element structure in 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;
}

它在stylechildren上显示错误

'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没有任何字符串作为属性,它为它定义了特定的属性。

您可以通过将elementanyelement.styleany来逃避 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 知道您的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 支持字符串文字类型,它扩展了字符串。 所以你可以使用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>>> 这是一口,但我们稍后会分解它。

现在,我们有PartialOmit业务。 要分解它,

  • 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>属性的属性——即tagstylechildren 这很重要,因为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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM