Typescript 根据同一对象中的属性推断函数的对象键

Posted

技术标签:

【中文标题】Typescript 根据同一对象中的属性推断函数的对象键【英文标题】:Typescript infer object keys for function based on properties in the same object 【发布时间】:2020-12-28 17:27:01 【问题描述】:

编辑:重新编写问题以使其更加清晰,并添加了一些额外的示例。

我想创建一个大对象(这是底层系统的要求),我想将该对象的两个元素之间的一种关系建模为由 TypeScript 引擎自动推断的 TypeScript 接口/类型。

我的对象如下所示:


  selection: 
    name: "",
    email: ""
  ,
  handler: (values) => 
    // Do something
  

我想要定义一个类型,以便将selection 对象中定义的键(在此示例中为nameemail,但可能更多)被推断为values 上的键对象。

例如:


  selection: 
    name: "",
    email: ""
  ,
  handler: (values) => 
    values.name // Valid
    values.email // Valid
    values.foobar // Not valid
  


// or


  selection: 
    name: "",
    email: ""
    address: "",
    role: ""
  ,
  handler: (values) => 
    values.name // Valid
    values.email // Valid
    values.address // Valid
    values.role // Valid
    values.foobar // Not valid
  

使用这种类型:

interface HandlerObject<TSelection extends  [key: string]: string > 
  selection: TSelection
  handler?: (selection:  [key in keyof TSelection]: any ) => any

只要我先明确设置通用TSelection,我就可以键入对象:

interface Selection 
  name: string
  email: string


const obj: HandlerObject<Selection> = 
  selection: 
    name: "",
    email: ""
  ,
  handler: (values) => 
    values.name // Valid
    values.email // Valid
    values.foobar // Not valid
  

然而,对我来说,似乎 TypeScript 应该能够根据我在selection 属性中键入的内容推断出TSelection 本身。虽然我当然可能是错的。我推测我应该能够做到这一点:

const obj: InferredHandlerObject = 
  selection: 
    name: "",
    email: ""
  ,
  handler: (values) => 
    values.name // Valid
    values.email // Valid
    values.foobar // Not valid
  

并且仍然以某种方式获得nameemail 的正确类型推断。这是可能的还是我需要明确输入选择?


原始问题

// SomeType is just a placeholder. This is where I want the proper type to go.
const obj: AType = 
  selection: 
    name: "something", // The value here is irrelevant. The key is important
    email: "something elese" // The value here is irrelevant. The key is important
  ,
  handler: (selection) => 
    selection.name // selection only contains keys from the selection object defined above
  

有没有一种方法可以使用 Typescript 以这样的方式键入此内容,即选择 argument 始终包含与 selection 属性中定义的键相同的键?参数中键的值也不应该相同。

我试过了:

interface HandlerObject<TSelection extends  [key: string]: string > 
    selection: TSelection
    handler?: (selection:  [key in keyof TSelection]: any ) => any

但它需要我明确指定泛型。

我希望它根据selection 属性自动推断TSelection

【问题讨论】:

SomeType的定义是什么? 它只是一个占位符,表示正确的类型应该放在哪里。我已通过澄清更新了问题。 嗯,这是个问题。如果您没有obj 的类型,则无法在对象初始化程序中引用它来获取类型信息。你得到 "'obj' 隐含类型 'any' 因为它没有类型注释并且在其自己的初始化程序中直接或间接引用。(7022)" 看起来是这样的:typescriptlang.org/play?#code/… 【参考方案1】:

您曾说过SomeType 是“属性类型应放在的位置的占位符”。如果obj 是一次性的,则根本不需要类型; TypeScript 将从对象初始化器中推断出它。但是我们需要将它拆分一下,否则你会试图从它自己的初始化程序中引用它,而 TypeScript 不会让你这样做。你得到 "'obj' 隐含类型 'any' 因为它没有类型注释并且在其自己的初始化程序中直接或间接引用。(7022)" (playground link)。

下面是我们如何在类型声明中使用 typeof 来做到这一点,方法是分开一点:

const initialSelection = 
    name: "select.name",
    email: "select.email"
;
const obj = 
    selection: initialSelection,
    handler: (selection: typeof initialSelection) => 
        selection.name // This works
    
;

Playground Link

如果您想避免在该范围内出现initialSelection,您可以将其包装在一个函数中:

const obj = (() => 
    const initialSelection = 
        name: "select.name",
        email: "select.email"
    ;
    return 
        selection: initialSelection,
        handler: (selection: typeof initialSelection) => 
            selection.name // This works
        
    ;
);

Playground Link

【讨论】:

这接近我想要的,但我希望 TypeScript 可以直接推断另一个对象属性的类型,从而避免需要匿名函数或与实际对象分开的显式定义。 @Thomas - 很确定你不能(我很遗憾看到)。尝试时 TypeScript 的错误非常明显(我很高兴看到)。 :-) 这就是我害怕的。感谢您帮助我调查。【参考方案2】:

如果您坚持将所有内容都放在一个对象中,则需要像这样定义SomeType

interface SomeType 
  selection: 
    name: string;
    email: string;
  ,
  handler: (selection: SomeType['selection']) => void;


const obj: SomeType = 
  selection: 
    name: "select.name",
    email: "select.email"
  ,
  handler: (param) => 
    param.name;
    param.email;
  

Playground

【讨论】:

理想情况下,我希望将其保存在单个对象中,因为这就是我正在使用的框架的运行方式 (Sanity.io)。我知道我可以这样输入或使用多种类型,但我希望可以直接推断它。 TypeScript 应该已经拥有所有必要的数据。 只是为了清楚。我完全不介意定义类型,但我想避免在使用 selection 属性时为它定义新类型。 @Thomas 认为这是不可能的,但你可以使用像 this 这样的泛型。 还有,这个一个对象,接口不是对象。 我知道我可以使用泛型,但这仍然需要我重新定义恕我直言应该可由 TypeScript 本身推断的属性。现在我将简单地坚持“任何”,因为输入两次很快就会变得烦人。无论如何感谢您的帮助。【参考方案3】:

不完全确定您追求的是什么,但我假设您知道“选择”将进入什么内容,并且可能会因情况而异,您希望确保处理程序仅使用现有属性。

interface SomeType<T> 
    selection: T,
    handler: (selection: T) => void


type User 
    name: string;
    email: string;



const obj: SomeType<User> = 
  selection: 
    name: "select.name",
    email: "select.email"
  ,
  handler: (selection) => 
    selection.name // selection only contains keys from the selection object defined above
  

【讨论】:

我已经更新了这个问题,希望能让事情变得更清楚。我试图让 TypeScript 为我推断 T 的键的繁重工作,因为我在对象本身中明确定义它们。【参考方案4】:

无论出于何种原因,泛型类型推断对函数的操作与对类型的操作不同。

解决办法如下:

function isObj<O>(obj:  selection: O; handler: (selection:  [key in keyof O]: any ) => any ) 
  return obj;


const obj = isObj(
  selection: 
    name: '',
    email: '',
  ,
  handler(values) 
    values.name; // Valid
    values.email; // Valid
    values.foobar; // Not valid
  ,
);
obj;

【讨论】:

以上是关于Typescript 根据同一对象中的属性推断函数的对象键的主要内容,如果未能解决你的问题,请参考以下文章

TypeScript 中对象字面量的动态泛型类型推断

TypeScript 中泛型类型的子类型推断

TypeScript 学习笔记 — 类型推断和类型保护

TypeScript 类型推断 - 函数的通用对象

typescript 从子类调用的构造函数参数推断类型

TypeScript——类型检查机制