【问题标题】:Type for a dot separated path to type键入以点分隔的路径键入
【发布时间】:2021-12-08 12:42:32
【问题描述】:

我想强烈键入指向特定类型的点分隔路径。假设我们有一个递归结构,其中每个叶子都是一个特定的类型,在这种情况下由一组语言翻译:

type Language = "pl" | "en";

type Translation = { [ lang in Language ]: string };
type Translations = { [key: string]: Translation | Translations | undefined };

这方面的一个例子可能是:

const translations = {
  hello: {
    pl: "Dzieńdobry",
    en: "Hello",
  },
  bool: {
    yes: {
      pl: "Tak",
      en: "Yes",
    },
    no: {
      pl: "Nie",
      en: "No",
    },
  },
};

我想要路径类型:"hello""bool.yes""bool.no",但 不是"bool""missing""bool.foo" or "hello.pl"` .到目前为止,这是我所拥有的:

对于单层翻译文件:

type KeyToTranslation<T extends Translations, K extends string = string> = K extends keyof T 
  ? T[K] extends Translation
    ? K
    : never
  : never;

function printTranslationByKey<T extends Translations, K extends string>(
  t: T,
  k: KeyToTranslation<T, K>
) {
    console.log(t[k]);
}
printTranslationByKey(translations, "hello"); // Valid, Correct!
printTranslationByKey(translations, "hello.pl"); // Error, Correct!
printTranslationByKey(translations, "missing"); // Error, Correct!
printTranslationByKey(translations, "bool.yes"); // Error, Incorrect.

所以我们需要一些模板字符串和infers。不幸的是,我似乎无法让这个工作。似乎忘记了我已经断言T[TKey] extends Translations

type DeepKeyToTranslation<T extends Translations, K extends string = string> =  K extends keyof T
    ? T[K] extends Translation
        ? K
        : never
    :
      // This is where we extend this further to cover the dot separated case:
      
      K extends `${infer TKey}.${infer Rest}`
        ? TKey extends keyof T
            ? T[TKey] extends undefined
                ? never
                : T[TKey] extends Translations
                    ? Rest extends DeepKeyToTranslation<T[TKey], Rest>
                        ? K
                        : never
                    : never
            : never
        : never;

function printTranslationByDeepKey<T extends Translations, K extends string>(
  t: T,
  k: DeepKeyToTranslation<T, K>
) {
    console.log(t[k]);
}
printTranslationByDeepKey(translations, "hello"); // Good!
printTranslationByDeepKey(translations, "hello.pl"); // Error
printTranslationByDeepKey(translations, "missing"); // Error

失败是因为:

Type 'T[TKey]' does not satisfy the constraint 'Translations'.
  Type 'T[string]' is not assignable to type 'Translations'.
    Type 'Translation | Translations | undefined' is not assignable to type 'Translations'.
      Type 'undefined' is not assignable to type 'Translations'.()

我发现了其他几个类似的问题:

第二个甚至是我当前实现的基础,但两者都没有提供类似的功能,您可以根据路径中的值的类型停止递归。

【问题讨论】:

    标签: typescript


    【解决方案1】:

    我不完全确定您的Translations 类型的| undefined 部分是否需要(这里可能有误),我怀疑这是造成大部分麻烦的原因。

    我稍微修改了您的示例并达到了一个似乎有效的程度,看看它是否涵盖了您的用例!

    type Language = "pl" | "en";
    
    type Translation = { [lang in Language]: string };
    type Translations = { [key: string]: Translation | Translations };
    
    type DeepKeyToTranslation<T extends Translations, K extends string = string> = K extends keyof T
        ? T[K] extends Translation
            ? K
            : never
        : K extends `${infer TKey}.${infer Rest}`
            ? T[TKey] extends Translations
                ? DeepKeyToTranslation<T[TKey], Rest> extends never
                    ? never
                    : K
                : never
            : never
    
    declare function printTranslationByDeepKey<T extends Translations, K extends string>(
        t: T,
        k: DeepKeyToTranslation<T, K>
    ): K
    
    const translations = {
        hello: {
            pl: "Dzieńdobry",
            en: "Hello",
        },
        bool: {
            yes: {
                pl: "Tak",
                en: "Yes",
            },
            no: {
                pl: "Nie",
                en: "No",
            },
        },
        nested: {
            deep: {
                ok: {
                    pl: "Dzieńdobry",
                    en: "Hello",
                }
            }
        }
    };
    
    
    printTranslationByDeepKey(translations, "hello") // ok
    printTranslationByDeepKey(translations, "bool.yes") // ok
    printTranslationByDeepKey(translations, "hello.pl") // err
    printTranslationByDeepKey(translations, "missing") // err
    printTranslationByDeepKey(translations, "nested.deep.ok") // ok
    

    Playground Link

    【讨论】:

    • 让我发疯,打字稿无法识别给定路径可能未定义,我试图避免一些我认为你是对的事情,如果我只使用 Translations 作为类型约束而不是实际类型...const x: Translations = ...
    • 这让我得到了我需要的洞察力。谢谢!
    【解决方案2】:

    考虑这个解决方案:

    type Language = "pl" | "en";
    
    
    const translations = {
      hello: {
        pl: "Dzieńdobry",
        en: "Hello",
      },
      bool: {
        yes: {
          pl: "Tak",
          en: "Yes",
        },
        no: {
          pl: "Nie",
          en: "No",
        },
      },
    } as const;
    
    type Translations = typeof translations;
    
    type KeysUnion<T, Cache extends string = ''> =
      T extends PropertyKey ? Cache : keyof T extends Language ? Cache : {
        [P in keyof T]:
        P extends string
        ? Cache extends ''
        ? KeysUnion<T[P], `${P}`>
        : Cache | KeysUnion<T[P], `${Cache}.${P}`>
        : never
      }[keyof T]
    
    type Result = KeysUnion<Translations>
    
    function printTranslationByDeepKey<T extends Translations, K extends KeysUnion<Translations>>(
      t: T,
      k: K
    ) {}
    printTranslationByDeepKey(translations, "hello"); // Good!
    printTranslationByDeepKey(translations, "hello.pl"); // Error
    printTranslationByDeepKey(translations, "missing"); // Error
    

    Playground

    KeysUnion - 递归遍历 Translations 类型。 如果Cache extends "" - 这意味着它是第一次迭代,我们需要使用适当的参数递归调用KeysUnion,但不使用Cache memoization,因为在结果中包含空字符串是没有意义的。 如果Cache 不是空字符串,则将Cache 应用于结果(将其与结果联合)并使用新后缀调用KeysUnion&lt;T[P], ${Cache}.${P}&gt;

    T extends PropertyKey - 如果T 不再是对象 - 返回Cache keyof T extends Language - 如果 T 是带有 pl/en 键的对象 - 无需将它们应用于结果 - 返回 Cache

    您可以使用 cmets here 找到此实用程序类型的另一种解释

    你可以在我的blog找到更多解释

    相关答案列表:[first,second,third,fourth]

    【讨论】:

    • 对。我特别希望递归在碰到我手上任何形状的物体时停止。翻译不是这里唯一的用例。而且我完全可以看到有问题的对象的键在树中合法高于叶子的情况。例如,"translations.languages.pl"enpl 键用于在英语 (Polish) 和波兰语 (Polski) 中说“波兰语”。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-08-02
    • 1970-01-01
    • 2018-08-22
    • 2020-06-05
    • 1970-01-01
    • 1970-01-01
    • 2021-05-12
    相关资源
    最近更新 更多