我的previous question关于TypeScript是什么开始了这个主题. 在上一个问题中,我想创建一个通用的sort()方法,它将与排序键数组一起工作,由您在这里看到的SortKey类型定义. 在上述问题中得到的结果只适用于该类型的直接性质.

现在,我想继续这个过程,看看我是否可以使它递归,以便我可以指定嵌套对象的键. 这就是我的工作:

type Comparer<T> = (a: T, b: T) => number;

type KeyPath<T, TParentKey = undefined> = {
    [K in keyof T]: K extends string ? (TParentKey extends undefined ? `${K}` : `${TParentKey & string}.${K}`) : never;
}[keyof T]
    | {
    [K in keyof T]: T[K] extends object ? K extends string ? KeyPath<T[K], TParentKey extends undefined ? `${K}` : `${TParentKey & string}.${K}`> : never : never;
}[keyof T]
;

type ValueAtPath<T, Path> = Path extends keyof T
    ? T[Path]
    : Path extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
        ? ValueAtPath<T[Key], Rest>
        : never
    : never;

type Getter<T, K extends string> = (model: T, key: K) => ValueAtPath<T,K>;

// This type is not working and is the subject for this question:
type SortKey<TModel> = {
    [K in KeyPath<TModel>]: {
        key: K;
        order: 'asc' | 'desc';
        getter: Getter<TModel, K>;
        comparer: Comparer<ValueAtPath<TModel, K>>;
    }
}[KeyPath<TModel>];

那么,我怎么知道SortKey个里面使用的"个体类型"有效呢? 像这样:

// Let's define a couple of data models.
type User = {
    id: number;
    name: {
        first: string;
        last: string;
    };
    emails?: string[];
    isActive: boolean;
    type: 'customer' | 'support'
};

type Domain = {
    id: number;
    value: string;
    user: User;
};

// Now let's start testing the TS types.
type TestKey = KeyPath<Domain>;

// Visual Studio Code shows in Intellisense the following for the above type:
type TestKey = "id" | "value" | "user" | KeyPath<User, "user">

// Although unsure why it did not expand the recursion, Intellisense fully works on a variable:
const k: TestKey = 'user.name'; // <-- See picture 1.

// This also seems OK as per the Intellisense tooltip:
type TestValue = ValueAtPath<Domain, 'user.name.last'>;
// The tooltip reads it is string, which is the data  type of the `last` nested property:
type TestValue = string

// Now testing Comparer:
const comp: Comparer<ValueAtPath<Domain, 'user.name'>> = (a, b) => 0;
// Intellisense shows:
const comp: Comparer<{
    first: string;
    last: string;
}>

// Finally, testing Getter:
const getter: Getter<Domain, 'user.name.first'> = m => m.user.name.first;
// Intellisense works perfectly on the parameter variable "m".

Picture 1:  100% of all keys in Domain

好吧,所以人们会认为,扩展到type DomainSortKey = SortKey<Domain>会工作. 但事实并非如此 TypeScript说DomainSortKey === unknown

百万美元的问题是:在Jebus发生了什么?

推荐答案

当你有一个mapped type的形式{[K in keyof T]: ⋯},其中直接有in keyof,它将是一个homomorphic mapped type,如What does "homomorphic mapped type" mean?所描述的.这意味着输入类型中的readonlyoptional属性在输出类型中将变为只读和可选属性. 可选属性在其域中始终包含undefined.

所以这意味着如果T的任何键(甚至可能是子键,给定递归)是可选的,那么KeyPath<T>类型将最终包含undefined.您可以测试该类型以查看这一点(看起来您试图这样做,但没有完全展开该类型.一种方法是用{} | null | undefinedintersect加到{} | null | undefined上,这种类型或多或少相当于unknown):

type TestKey = KeyPath<Domain> & ({} | null | undefined);
//   ^? type TestKey = "id" | "value" | "user" | "user.id" | "user.name" |
//      "user.emails" | "user.isActive" | "user.type" | "user.name.first" | 
//      "user.name.last" | undefined

现在,理想情况下,TypeScript足够聪明,可以防止你在indexed access type中使用undefined作为键.但有时候,如果泛型足够复杂,编译器就看不到问题所在:

type Hmm<T> = { [K in keyof T]: K }[keyof T]
type Grr<T> = T[Hmm<T>]; // <-- this should be an error but it's not
type Okay = Grr<{a: string}> // string
type Bad = Grr<{ a?: string }> // unknown

unknown是当编译器试图找出{a?: string}[undefined]时发生的事情.根据this comment on microsoft/TypeScript#56515的说法,允许undefined作为密钥潜入是一个已知的设计限制,因为试图修复它会在现实世界的代码中造成太多的 destruct .所以他们允许这样做,并建议如果你正在编写一个类型函数来提取密钥,你应该执行以下修复:


这里推荐的修复方法是使用-? mapping modifier来使映射中需要的密钥,并防止undefined被包含在域中.(-?修饰语用于the Required<T> utility type的定义):

type KeyPath<T, TParentKey = undefined> = {
  [K in keyof T]-?: K extends string ? (
    //          ^^
    TParentKey extends undefined ? `${K}` : `${TParentKey & string}.${K}`
  ) : never; }[keyof T]
  | {
    [K in keyof T]-?: T[K] extends object ? (
      //          ^^
      K extends string ? KeyPath<
        T[K], TParentKey extends undefined ? `${K}` : `${TParentKey & string}.${K}`
      > : never
    ) : never;
  }[keyof T];

从密钥列表中删除undefined个:

type TestKey = KeyPath<Domain> & ({} | null | undefined);
//   ^? type TestKey = "id" | "value" | "user" | "user.id" | "user.name" |
//      "user.emails" | "user.isActive" | "user.type" | "user.name.first" | 
//      "user.name.last" 

SortKey:

type DomainSortKey = SortKey<Domain>;
/* type DomainSortKey = {
    key: "id";
    order: 'asc' | 'desc';
    getter: Getter<Domain, "id">;
    comparer: Comparer<number>;
} | {
    key: "value";
    order: 'asc' | 'desc';
    getter: Getter<Domain, "value">;
    comparer: Comparer<...>;
} | ... 7 more ... | {
    ...;
} */

Playground link to code

Typescript相关问答推荐

如何使打包和拆包功能通用?

如何使用axios在前端显示错误体

typescribe不能使用值来索引对象类型,该对象类型满足具有该值的另一个类型'

在NextJS中获得Spotify Oauth,但不工作'

根据上一个参数值查找参数类型,也返回类型

如何设置绝对路径和相对路径的类型?

ANGLE独立组件布线错误:没有ActivatedRouting提供程序

以Angular 执行其他组件的函数

TypeScrip:强制使用动态密钥

如何在省略一个参数的情况下从函数类型中提取参数类型?

棱角分明-什么是...接口类型<;T>;扩展函数{new(...args:any[]):t;}...卑鄙?

如何从接口中省略接口

如何从上传文件中删除预览文件图标?

Typescript .根据枚举输入参数返回不同的对象类型

埃斯林特警告危险使用&as";

如何在TypeScrip中使用数组作为字符串的封闭列表?

我可以使用JsDocs来澄清Typescript实用程序类型吗?

TypeScript中的这些条件类型映射方法之间有什么区别?

如何在TypeScript类成员函数中使用筛选后的keyof this?

如何强制对象数组中的对象属性具有非空字符串值?