【问题标题】:Type for key with suffix带后缀的键类型
【发布时间】:2021-08-11 06:33:54
【问题描述】:

打字稿是否可以定义state的键应该是lowercase + some string

type HasSufix = `${Lowercase<string>}Required`

interface SomeShape {
  [key: HasSufix]: boolean
}

const state: SomeShape = {
  usersRequired: false,
  ordersRequired: false,
  booksRequired: false,
};

【问题讨论】:

    标签: javascript typescript


    【解决方案1】:

    TypeScript 中目前没有与您想要的 SomeShape 类型相对应的特定类型。 Lowercase&lt;string&gt; 仅计算为 string;即使这不是真的,pattern template literal types`${string}Required` 目前也不能用作对象的键类型;请参阅microsoft/TypeScript#42192 了解更多信息。

    相反,您可以将SomeShape 表示为generic 类型,作为候选类型的约束。也就是说,你创建一个类似ValidSomeShape&lt;T&gt; 的类型,这样T extends ValidSomeShape&lt;T&gt; 当且仅当T 是一个有效的SomeShape。它可能看起来像这样:

    type ValidSomeShape<T extends object> = { [K in keyof T as
      K extends `${infer P}Required` ? `${Lowercase<P>}Required` :
      `${Lowercase<Extract<K, string>>}Required`]:
      boolean
    } extends infer O ? {[K in keyof O]: O[K]} : never;
    

    这个工作的方式是:编译器re-maps the keysT 到那些是有效的;如果键 K 不以 "Required" 结尾,那么我们追加它。否则,我们将"Required" 之前的部分转换为自身的小写版本。我们确保属性类型是boolean

    extends infer O ? ... 结尾的部分是trick from the answer to another question,它鼓励编译器在IntelliSense 中列出ValidSomeShape&lt;T&gt; 的实际属性,而不是显示相当不透明的ValidSomeShape&lt;T&gt; 名称。您宁愿在错误消息中看到 {fooRequired: boolean} 而不是 ValidSomeShape&lt;{foo: string}&gt;


    继续:为了避免人们不得不手动指定T,您可以创建一个通用辅助函数asSomeShape()从其输入推断T

    const asSomeShape = <T extends ValidSomeShape<T>>(obj: T) => obj;
    

    所以不要注释const state: SomeShape = {...},而是写const state = asSomeShape({...})


    让我们试试吧:

    const state = asSomeShape({
      usersRequired: false,
      ordersRequired: false,
      booksRequired: false,
    }); // okay
    

    这编译没有错误。但是请注意当您做错事时会发生什么:

    const badState1 = asSomeShape({
      usersRequired: false,
      ordersRequired: 123, // error!
    //~~~~~~~~~~~~~~ <-- // Type 'number' is not assignable to type 'boolean'
      booksRequired: false,
    }); // okay
    
    const badState2 = asSomeShape({
      usersRequired: false,
      ordersRequired: false,
      BooksRequired: false, // error!
    //~~~~~~~~~~~~~~~~~~~~
    // Object literal may only specify known properties, but 'BooksRequired' does not exist in type 
    // '{ usersRequired: boolean; ordersRequired: boolean; booksRequired: boolean; }'. 
    // Did you mean to write 'booksRequired'?
    }); // okay
       
    const badState3 = asSomeShape({
      users: false, // error!
    //~~~~~~~~~~~~
    // Object literal may only specify known properties, and 'users' does not exist in type 
    // '{ usersRequired: boolean; ordersRequired: boolean; booksRequired: boolean; }'
      ordersRequired: false,
      booksRequired: false,
    }); // okay
    

    您可以看到每次失败都会导致有用的错误消息。 ordersRequired 属性是 number 而不是预期的 booleanBooksRequired 属性可能应该是 booksRequired;并且users 属性也是错误的(编译器似乎认为它与usersRequired 不够接近,暗示您应该改写它,但它确实说它希望在那里看到usersRequired)。


    所以,至少在 TypeScript 4.2 中,这已经差不多了。

    由于使用通用约束比特定类型更复杂,您可能只想在与尚未验证的对象交互的函数中使用ValidSomeShape&lt;T&gt;...例如某些面向外部的端点图书馆。验证对象后,您可以将其类型扩展为不太精确但非泛型类型,如 Record&lt;string, boolean&gt; 或其他类型,并将其作为更广泛的类型在库中传递:

    export function userFacingLibraryFunction<T extends ValidSomeShape<T>>(someShape: T): void {
      // now that someShape has been validated, we can pass it to our internal functions:
      internalLibraryFunction(someShape);
    }
    
    // not exported
    function internalLibraryFunction(alreadyValidatedSomeShape: Record<string, boolean>): void {  
      Object.keys(alreadyValidatedSomeShape).filter(k => alreadyValidatedSomeShape[k]);
    }
    

    Playground link to code

    【讨论】:

      【解决方案2】:

      您需要定义形状中应允许哪些键:

      type HasSufix<Key extends string> = `${Lowercase<Key>}Required`;
      
      type Prefixes = 'USERS' | 'ORDERS' | 'BOOKS';
      
      type SomeShape = {
        [Key in HasSufix<Prefixes>]: boolean;
      }
      
      const state: SomeShape = {
        UsersRequired: false, // error
        ordersRequired: false,
        booksRequired: false,
      };
      

      如果您不定义允许使用哪些键,即如果您将 type Prefixes = 'USERS' | 'ORDERS' | 'BOOKS'; 替换为 type Prefixes = string;,它将不再起作用,因为类型 Lowercase&lt;string&gt; 等于 string(如在 @987654321 中可见@ 或 TS playground),因此也允许非小写字符串作为键。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-07-17
        • 2021-07-26
        • 2014-02-04
        • 1970-01-01
        • 2011-03-17
        • 2013-05-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多