繁体   English   中英

打字稿智能感知无法根据条件推断条件类型的通用类型

[英]Typescript intellisense cannot infer generic type of conditional types based on condition

我已经设置了一个接口,它接受一个扩展联合的泛型。 在那个泛型上,我创建了一组条件类型。

type SessionContentType = "a" | "b" | "c";

interface SessionDetails<T extends SessionContentType> {
    id: string;
    sectionId?: string;
    segments?: string[];
    content: {
        type: T;
        contentId?: T extends Exclude<SessionContentType, 'b'> ? string : never;
        channelId?: T extends Exclude<SessionContentType, 'a'> ? string : never;
        suffix?: T extends Exclude<SessionContentType, 'a' | 'c'> ? string : never;
    }
}

到这里,似乎一切都很好。 SessionDetails["content"]["suffix"]具有以下类型:

在此处输入图片说明

因此,泛型必须是'b'类型,后缀不能是never

然后我创建了一个函数,该函数使用符合SessionDetails且具有已知T

function getSuffixFromSession(
    session: SessionDetails<Exclude<SessionContentType, 'a'>>
) {
    if (session.content.type === 'c') {
        session.content.type; // This is correctly of type 'c'
        session.content.suffix; // I expect this to be of type 'never' but it is `string | undefined`

        return '';
    }

    const suffix = session.content.suffix ?? '';
    return suffix && `-${suffix}`;
}

为了确认我做的一切都是正确的,我添加了这两个详细的语句来查看它们的类型。

session.content.type正如预期的那样,属于'c'类型。 但是如果typec类型(因此这应该可以写为SessionDetails<'c'> ),为什么session.content.suffix解析为类型string | undefined string | undefined而不是never (或never | undefined ,实际上?)?

如果我做

function getSuffixFromSession(
    session: SessionDetails<'c'>
) {
    if (session.content.type === 'c') {
        session.content.type; // This is correctly of type 'c'
        session.content.suffix; // I expect this to be of type 'never' but it is `undefined`

        return '';
    }

    const suffix = session.content.suffix ?? '';
    return suffix && `-${suffix}`;
}

session.content.suffixundefined而不是never ,但这仍然不是我想要的。

我做错了什么或者我假设有什么问题吗?

这是游乐场网址。

谢谢你。

这里有几件事情正在发生。


一是您的SessionDetails<T>类型实际上并没有像您认为的那样限制事物。 T是像"b" | "c"这样的联合时"b" | "c" "b" | "c"SessionDetails<T>["content"]类型是单个对象类型而不是联合:

type Content = SessionDetails<Exclude<SessionContentType, 'a'>>["content"];
/* type Content = {
  type: "b" | "c";
  contentId?: string;
  channelId?: string;
  suffix?: string;
} */

这意味着编译器可以接受:

const sd: SessionDetails<"b" | "c"> = {
    id: "",
    content: {
        type: "c",
        suffix: "oopsie" // <-- definitely a string
    }
}; // no error
getSuffixFromSession(sd); 

由于当T"b" | "c"时, content.suffix条件类型评估为string "b" | "c" "b" | "c" ,那么即使你在getSuffixFromSession()的实现中验证session.content.type"c"suffix的类型也不依赖于它, 控制流分析什么也不做。


undefinednever是,当您读取碰巧丢失的可选属性时,您将获得undefined值:

interface Foo { bar?: never };
const foo: Foo = {}
console.log(foo.bar) // undefined

never类型表示不可能的条件; 没有类型为never值,如果您在代码中到达编译器认为值类型为never ,那么要么编译器出错,要么在运行时永远不会到达该位置。

当您将可选属性声明为never类型时,您实际上是在说您希望该属性根本不存在。 undefined分配给这样的属性有一些细微差别; 当前允许使用--strictNullChecks但 TS4.4 将引入一个--exactOptionalPropertyTypes编译器标志来阻止它。 但是无论哪种方式,当您读取类型为never的可选属性时,您肯定会得到一个undefined

因此,接收undefined是期望的行为。


无论如何,在 TypeScript 中获得这种“检查对象的一个​​属性以缩小整个对象的类型”功能的唯一方法是使用可区分的 union 如果你希望你content属性是工会,而不是一个单一的对象类型,则需要发布在任何联盟,物业类型T 分布式条件类型将自动执行此操作:

interface SessionDetails<T extends SessionContentType> {
    id: string;
    sectionId?: string;
    segments?: string[];
    content: T extends any ? { // <-- distributes
        type: T;
        contentId?: T extends Exclude<SessionContentType, 'b'> ? string : never;
        channelId?: T extends Exclude<SessionContentType, 'a'> ? string : never;
        suffix?: T extends Exclude<SessionContentType, 'a' | 'c'> ? string : never;
    } : never
}

您可以验证:

type Content = SessionDetails<Exclude<SessionContentType, 'a'>>["content"];
/* type Content = {
  type: "b";
  contentId?: undefined;
  channelId?: string | undefined;
  suffix?: string | undefined;
} | {
  type: "c";
  contentId?: string | undefined;
  channelId?: string | undefined;
  suffix?: undefined;
} */

因此以下分配失败:

const sd: SessionDetails<"b" | "c"> = {
    id: "",
    content: { // error!
  //~~~~~~~ <--  Types of property 'suffix' are incompatible.
        type: "c",
        suffix: "oopsie"
    }
}

getSuffixFromSession()实现中的控制流分析按需要进行:

function getSuffixFromSession(
    session: SessionDetails<Exclude<SessionContentType, 'a'>>
) {
    if (session.content.type === 'c') {
        session.content.type;
        session.content.suffix; // undefined
        return '';
    }
}

Playground 链接到代码

您几乎是正确的,您只想将string交换为T

像这样:

type SessionContentType = "a" | "b" | "c";

interface SessionDetails<T extends SessionContentType> {
    id: string;
    sectionId?: string;
    segments?: string[];
    content: {
        type: T;
        contentId?: T extends Exclude<SessionContentType, 'b'> ? T : never;
        channelId?: T extends Exclude<SessionContentType, 'a'> ? T : never;
        suffix?: T extends Exclude<SessionContentType, 'a' | 'c'> ? T : never;
    }
}


function getSuffixFromSession(
    session: SessionDetails<Exclude<SessionContentType, 'a'>>
) {
    if (session.content.type === 'c') {
        session.content.type; // => 'c'
        session.content.suffix; // => 'b'

        return '';
    }

    const suffix = session.content.suffix ?? '';
    return suffix && `-${suffix}`;
}

TS游乐场

由于 never 不会直接显示,您可以将never替换为number ,您可以看到以下内容类型接口

我还没有深入研究 TypeScript 编译器源代码,但我猜测 typescript 将T推断为'b' | 'c' 'b' | 'c'在这里,所以session被推断为SessionDetails<'b' | 'c'> SessionDetails<'b' | 'c'> ,最终session.content.suffix将被推断为string | never | undefined string | never | undefined

如果你真的想要这样的 impl,也许这是一个更好的方法,明确告诉 TypeScript session是一个SessionDetail<'c'>

(session as SessionDetails<'c'>).content.suffix; // (property) suffix?: never | undefined

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM