【问题标题】:Enforcing types of interface value强制执行接口值的类型
【发布时间】:2021-09-03 04:00:08
【问题描述】:

正在学习 Typescript,并试图了解如何基于接口强制执行类型。

我有这段代码

    interface User{
    name:string;
    age:number;
    friends:string[]
    hasPet:boolean
}

function setValue<T>(key:keyof T, value: T[keyof T] ):void{
    console.log(key, value)
    // ...some additonal logic
}

setValue<User>("age", true )  //should throw error but doesn't

我希望这个接口在我调用setValue 时强制执行密钥类型。这主要是可行的,但我可以将任何可用的用户类型传递给设置值。

我期望的是,当“年龄”作为setValue 的第一个参数传递时,第二个参数应该限制为“数字”的类型。所有其他用户键也一样。

感谢您的帮助!

【问题讨论】:

  • 这是文档typescriptlang.org/docs/handbook/2/…的相关部分
  • 您能解释一下为什么要手动指定User 吗?在没有T 类型的值的情况下,像 T 这样的通用函数实际上可以做什么?您确实需要K extends keyof T 中的函数是通用的,但我不知道您是否真的需要T 是通用的,或者它是否可以硬编码为User。请参阅this code 了解各种选项,如果您希望其中任何一个作为答案,请告诉我。
  • @jcalz 该函数不必是通用的,那位是我正在尝试学习 TS 的实验。我认为您的HardcodedUserType 可以正常工作。谢谢!

标签: typescript typescript-generics


【解决方案1】:

您应该在 Typescript 游乐场中检查它。它没有抛出错误的原因keyof 返回一个表示所有属性键的类型。

所以你的函数实际上看起来像:

setValue(key: keyof User, value: string | number | boolean | string[])

显然,您尝试做的事情适合其中一种类型。

好像你试图做类似的事情:

let newUser: User;
newUser.age = 'abc'; // Type error

【讨论】:

  • 我想我意识到keyof 运算符正在返回所有可用的类型。我不知道如何用第一个参数的值来缩小第二个参数的可能类型。
【解决方案2】:

为了正确的类型推断,键也必须是类型变量。

让我猜猜,但在实际应用中,你很可能会有类似的功能

function setValue<T, K extends keyof T>(obj: T, key: K, value: T[K]) {
    obj[key] = value;
}

const user: User = {
    name: 'Den',
    age: 22,
    friends: ['1'],
    hasPet: true,
};

setValue(user, 'age', true); // Error - Argument of type 'boolean' is not assignable to parameter of type 'number'

因为在您的示例中,该函数具有键和值的参数,但没有要更改的对象。


如果您出于某种原因需要 setter 函数,则另一种解决方案:

const createValueSetter = <T>(obj: T) => <K extends keyof T>(
    key: K,
    value: T[K]
) => {
    obj[key] = value;
};

const user: User = {
    name: 'Den',
    age: 2,
    friends: ['1'],
    hasPet: true,
};

const userSetter = createValueSetter(user);

userSetter('age', true); // Error - Argument of type 'boolean' is not assignable to parameter of type 'number'

【讨论】:

    【解决方案3】:

    为了使其工作,您需要在key 输入的类型中创建函数generic。让我们更改您的示例,使其仅适用于设置 User 的属性,并使用该函数创建该类型的 user 来修改:

    const user: User = {
      name: "",
      age: 0,
      friends: [],
      hasPet: false
    }
    
    function setUserValue<K extends keyof User>(key: K, value: User[K]) {
      user[key] = value;
    }
    

    在这里您可以看到该函数在K 中是通用的,从constrainedkeyof Userkey 输入是 K 类型,value 输入是 User[K] 类型,indexed access type 表示“User 的属性类型在 K 类型的键上”。让我们看看它是如何工作的:

    setUserValue("age", true); // error, boolean isn't a number 
    setUserValue("age", 25) // okay
    

    看起来不错。编译器将K 推断为literal type "age",然后希望value 的类型为User["age"],即number

    简单版的回答到此结束。


    这是这个问题的标准解决方案;请注意,使用此功能仍然可以做不安全的事情,尽管可能性较小。例如,当key 属于union type 时,可能会出错:

    const key = Math.random() < 0.5 ? "age" : "hasPet";
    setUserValue(key, true); // no error!  But this has a 50% chance of doing something bad
    

    目前在 TypeScript 中没有直接的方式来说明我们希望 Kkeyof User 联合中的完全一个元素,并且不允许它本身是联合类型。 microsoft/TypeScript#27808 有一个功能请求,要求对此提供支持。现在你需要接受key 成为一个联合体(或者以一种复杂的方式阻止它,我不会在这里讨论)。

    真正的问题是索引访问类型T[K] 仅对读取 属性是安全的,因为T[K1 | K2] 变为T[K1] | T[K2]。但是对于writing,你真的希望T[K1 | K2] 被视为T[K1] &amp; T[K2]...也就是说,键类型中的联合应该成为“写入索引访问”类型中的intersections

    你可以在 TypeScript 中表达这样的类型函数如下:

    type WritingIdx<T, K extends keyof T> =
      { [P in K]: (x: T[P]) => void }[K] extends (x: infer I) => void ? I : never;
    

    (这里我使用与here 基本相同的技术将联合变为交叉点)

    然后你可以声明setUserValue() 使value 的类型为WritingIdx&lt;User, K&gt; 而不是User[K]

    function setUserValue<K extends keyof User>(key: K, value: WritingIdx<User, K>): void;
    function setUserValue<K extends keyof User>(key: K, value: User[K]) {
      user[key] = value;
    }
    

    请注意,我必须将其设为单呼签名overload,因为WritingIdx&lt;User, K&gt; 的一个缺点是当value 属于该类型时,编译器无法验证user[key] = value 是否安全。所以在实现中我将它扩大到User[K]

    我们来看看吧:

    setUserValue("age", true); // error, boolean isn't a number 
    setUserValue("age", 25) // okay
    setUserValue(key, true); //error!
    

    如果我们想象User 有一些具有重叠类型的属性:

    interface User {
      name: string;
      shoeSize: string | number;
    }
    

    这里shoeSize 可以是stringnumber,但name 只能是string。那么他们俩上的setUserValue()应该只接受交集,也就是string

    const nameOrShoeSize = Math.random() < 0.5 ? "name" : "shoeSize"
    setUserValue(nameOrShoeSize, "hasToBeAString"); // okay
    setUserValue(nameOrShoeSize, 123); // error
    

    那么,您需要这个更复杂的答案吗?可能不会,如果 key 参数只是一个字符串文字。即使有时会出现工会,你也可能不想把精力花在处理复杂而迂腐的代码库上。 TypeScript 一般allows unsafe things in the name of developer convenience,所以在类型系统中还有很多其他的“漏洞”。但我想我至少会谈谈与通用属性编写器函数有关的问题。

    Playground link to code

    【讨论】:

      猜你喜欢
      • 2016-08-13
      • 1970-01-01
      • 1970-01-01
      • 2017-11-05
      • 1970-01-01
      • 2016-12-09
      • 2019-09-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多