typescript 如何将已知的接口属性与自定义索引签名结合起来?

7kjnsjlb  于 2023-02-05  发布在  TypeScript
关注(0)|答案(4)|浏览(136)

如何键入一个可以同时具有几个声明的可选属性的对象,例如:

{ 
    hello?: string, 
    moo?: boolean 
}

以及自定义属性(必须是函数),例如:

[custom: string]: (v?: any) => boolean

这是我希望看到的,例如:

const myBasic: Example = {moo: false}
// -> ✅ Valid! Using known keys

const myValid: Example = {hello: 'world', customYo: () => true}
// -> ✅ Valid! "customYo" is a function returning a bool. Good job!

const myInvalid: Example = {hello: 'world', customYo: 'yo!'}
// -> ☠️ Invalid! "customYo" must be a function returning a boolean

尝试向具有已知键的接口(例如hello?: string, moo?: boolean)添加索引签名要求所有键都是索引签名类型的子集(在本例中,函数返回boolean),这显然会失败。

x3naxklr

x3naxklr1#

业主接受的问题(到目前为止)是不正确的。

以下是您的操作方法:

您需要使索引签名成为接口中可以包含的所有类型的联合类型:

interface IExample {
    hello?: string;
    moo?: boolean;
    [custom: string]: string | boolean | YourFunctionType;
}

interface YourFunctionType {
    (v?: any): boolean;
}

请注意,为了提高可读性,我已经将函数类型提取到了一个单独的接口中。

含义:

这意味着TS很好地支持显式定义的属性:

const test: IExample = <IExample>{};
test.hello.slice(2); // using a string method on a string --> OK
const isHello = test.hello === true; // ERROR (as expected): === cannot be applied to types string and boolean
const isMoo2 = test.moo === true; // OK

然而,现在需要使用类型保护来检查索引签名中的所有属性,这会增加一点运行时开销:

test.callSomething(); // ERROR: type 'string | boolean | YourFunctionType' has no compatible call signatures
if (typeof test.callSomething === 'function') { // alternatively you can use a user defined type guard, like Lodash's _.isFunction() which looks a little bit nicer
    test.callSomething(); // OK
}

另一方面:运行时开销是必要的,因为test可能是这样访问的:

const propertyName: string = 'moo';
test[propertyName](); // ERROR: resolves to a boolean at runtime, not a function ...

// ... so to be sure that an arbitrary propertyName can really be called we need to check:
const propertyName2: string = 'arbitraryPropertyName';
const maybeFunction = test[propertyName2];
if (typeof maybeFunction === 'function') {
    maybeFunction(); // OK
}
x0fgdtte

x0fgdtte2#

根据设计https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html,这是不可能的
一旦你有了一个字符串索引签名,所有的显式成员也必须符合这个索引签名。这是为了提供安全性,以便任何字符串访问都给出相同的结果。
解决这个问题的唯一方法是利用每个接口可以有两个单独的索引签名,一个用于string,另一个用于number
在本例中,hellomoo使字符串索引不可用,但您可以劫持自定义方法的数字索引

interface IExample {
  hello?: string
  moo?: boolean
  [custom: number]: (v?: any) => boolean
}

const myBasic: IExample = {moo: false}
// -> ✅ Valid! Using known keys

const myValid: IExample = {hello: 'world', 2: () => true}
// -> ✅ Valid! "customYo" is a function returning a bool. Good job!

const myInvalid: IExample = {hello: 'world', 2: 'yo!'}
// -> ☠️ Invalid! "customYo" must be a function returning a boolean

这是可行的,但几乎不是一个可接受的接口,因为这将导致不直观的函数,您将不得不通过数组符号来调用它们

myValid.7() // Cannot invoke an expression whose type lacks a call signature. Type 'Number' has no compatible call signatures.
myValid[2]() // works (but ewwwww what is this!!!)
// could alias to more readable locals later but still ewwwwww!!! 
const myCustomFunc = myValid[2]
myCustomFunc() // true

还有一点需要注意,从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型,这是因为当使用数字进行索引时,javascript会在索引到对象之前将数字转换为字符串
在这种情况下,您没有显式的字符串索引器,因此字符串索引类型是数字索引器类型可以遵循的缺省值any

重要提示这只是为了科学,我不建议将其作为真实的生活中的方法!

mjqavswn

mjqavswn3#

迟到了,但是你可以使用泛型,并使一些条件式工作

type Example<T extends string> = {
     [key in T]: key extends "hello"
       ? string
       : key extends "moo"
       ? boolean
       : (v?: any) => boolean
 }

这需要注意以下几点:

  • 这只在TypeScript可以自动推断泛型类型的上下文中才真正起作用:
// This works
function checkExample<T extends string>(arg: Example<T>): Example<T> {
    return arg;
}

// We can use it to automatically infer the generic type
// Here myExample has the correct type Example<...>
const myExample = checkExample({hello: 'world', customYo: () => true});

// This does not work, unless you're willing to type a union of all keys
const myExample: Example = {hello: 'world', customYo: () => true};
  • 这使得所有的键都是可选的。如果你需要一个必需的键,你将不得不与另一个类型相交:
type Example<T extends string> = {
    [key in T]: key extends "hello"
      ? string
      : key extends "moo" // We still need this here
      ? boolean
      : (v?: any) => boolean
} & {
    moo: boolean // We declare it here as well
}
rt4zxlrg

rt4zxlrg4#

实现@NicBright的答案的一个非常简单的方法是使用“高阶类型”,它可以用任何你想要的索引签名来扩展你已经定义的类型,例如:

type MyOptionalProperties = { 
    hello?: string, 
    moo?: boolean 
};

type WithCustomFunctions<T> = {
  [custom: number]: ((v?: any) => boolean) | T[keyof T];
} & T;

type MyOptionalPropertiesWithCustomFunctions = WithCustomFunctions<MyOptionalProperties>;

这种方法的好处是可以将高阶类型应用于其他结构(WithCustomFunctions的可重用性)。
坏消息是他的解决方案的所有含义仍然适用。

相关问题