简体   繁体   English

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

[英]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 : 这将是包含样式的对象,如( colorfontSize等)
  • Any other keys could be added to this object.(Like innerHTML , src etc.)可以将任何其他键添加到此对象。(如innerHTMLsrc等)

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它在stylechildren上显示错误

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.请解释每个问题及其修复方法。

Answering Your Questions回答您的问题

Assignability with index properties索引属性的可分配性

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.任何其他属性也可能存在,并且始终是字符串。

Indexing 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:您可以通过将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];
        }
    }

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.有时你必须这样做,但它应该总是让你不舒服。

Improving the Type Safety提高类型安全性

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 知道您的styleelement.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时,如果你使用了一个错误的属性——双赢,你会得到一个错误。

Generic Types通用类型

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" }调用它,你会得到一个错误。

Constraining things to 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" })我们会得到一个错误而不是它被默默接受然后不能正常工作。

Typing the rest correctly正确键入其余部分

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 .现在,我们有PartialOmit业务。 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>属性的属性——即tagstylechildren 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

Conclusion: Some examples/test cases结论:一些示例/测试用例

// 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.

相关问题 Typescript如何使用映射类型创建嵌套的接口对象? - Typescript how to create a nested interface objects using mapped types? 如何为此 Firebase 响应 JSON 构建 Typescript 接口? - How to structure a Typescript Interface for this Firebase Response JSON? 如何创建一个通用的 TypeScript 接口,该接口匹配任何类型/接口/对象,但其中的值类型有限制? - How to create a generic TypeScript interface which matches any type/interface/object with restrictions on types for values inside it? 如何在 typescript 中创建接口适配器? - How to create interface adapter in typescript? Typescript 接口树形结构 - Typescript interface for tree structure 打字稿:界面中的重载功能如何 - Typescript: How overload function in interface 打字稿 - 使用“for of”迭代会产生错误“TypeError: element is not a function” - Typescript - using the "for of" iternation creates the error "TypeError: element is not a function" 如何将字符串转换为 Typescript 中的复杂嵌套接口? - How to cast string to complex and nested interface in Typescript? 如何访问 React TypeScript 中的嵌套数据接口 - How to access nested data interface in React TypeScript 如何更新每个表单元素的状态以更改嵌套数据结构的正确属性,该数据结构是嵌套表单元素 DOM 的模型? - How to update at each form element's state change the correct property of a nested data-structure which is a model of the nested form element DOM?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM