简体   繁体   English

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

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

I've setup an interface that accepts a generic that extends a union.我已经设置了一个接口,它接受一个扩展联合的泛型。 On that generic, I created a set of conditional types.在那个泛型上,我创建了一组条件类型。

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;
    }
}

Up to here, it seems everything fine.到这里,似乎一切都很好。 SessionDetails["content"]["suffix"] has the following type: SessionDetails["content"]["suffix"]具有以下类型:

在此处输入图片说明

So, generic must be of type 'b' for suffix to not be never .因此,泛型必须是'b'类型,后缀不能是never

Then I create a function that uses a structure that complies to SessionDetails with a known T .然后我创建了一个函数,该函数使用符合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}`;
}

Just to confirm I was doing everything correctly, I added those two detailed statements to see their types.为了确认我做的一切都是正确的,我添加了这两个详细的语句来查看它们的类型。

session.content.type is, as expected, of type 'c' . session.content.type正如预期的那样,属于'c'类型。 But if type is of type c (and therefore this should be writable as SessionDetails<'c'> ), why does session.content.suffix resolves as type string | undefined但是如果typec类型(因此这应该可以写为SessionDetails<'c'> ),为什么session.content.suffix解析为类型string | undefined string | undefined instead of never (or never | undefined , actually?)? string | undefined而不是never (或never | undefined ,实际上?)?

If I do如果我做

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.suffix is undefined instead of never , but still this is not what I'd like to obtain. session.content.suffixundefined而不是never ,但这仍然不是我想要的。

Is there something wrong that am I doing or that probably I'm assuming?我做错了什么或者我假设有什么问题吗?

Here's the playground url. 这是游乐场网址。

Thank you.谢谢你。

Several things are going on here.这里有几件事情正在发生。


One is that your SessionDetails<T> type does not actually constrain things the way you think it does.一是您的SessionDetails<T>类型实际上并没有像您认为的那样限制事物。 When T is a union like "b" | "c"T是像"b" | "c"这样的联合时"b" | "c" "b" | "c" , the type of SessionDetails<T>["content"] is a single object type and not a union: "b" | "c"SessionDetails<T>["content"]类型是单个对象类型而不是联合:

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

Which means this is acceptable to the compiler:这意味着编译器可以接受:

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

Since your conditional type for content.suffix evaluates to string when T is "b" | "c"由于当T"b" | "c"时, content.suffix条件类型评估为string "b" | "c" "b" | "c" , then even if you verify that session.content.type is "c" inside the implementation of getSuffixFromSession() , the type of suffix does not depend on it, and control flow analysis does nothing. "b" | "c" ,那么即使你在getSuffixFromSession()的实现中验证session.content.type"c"suffix的类型也不依赖于它, 控制流分析什么也不做。


The issue having to do with undefined versus never is that when you read an optional property that happens to be missing, you will get a value of undefined :undefinednever是,当您读取碰巧丢失的可选属性时,您将获得undefined值:

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

The never type represents an impossible condition; never类型表示不可能的条件; there are no values of type never , and if you get to a place in your code where the compiler thinks a value is of type never , then either the compiler is mistaken, or that place will never be reached at runtime.没有类型为never值,如果您在代码中到达编译器认为值类型为never ,那么要么编译器出错,要么在运行时永远不会到达该位置。

When you have an optional property declared to be of type never , you are essentially saying that you expect the property not to exist at all.当您将可选属性声明为never类型时,您实际上是在说您希望该属性根本不存在。 There are some nuances around assigning undefined to such a property;undefined分配给这样的属性有一些细微差别; it's currently allowed with --strictNullChecks but TS4.4 will introduce an --exactOptionalPropertyTypes compiler flag that will prevent it.当前允许使用--strictNullChecks但 TS4.4 将引入一个--exactOptionalPropertyTypes编译器标志来阻止它。 But either way, when you read an optional property of type never , you are definitely going to get an undefined .但是无论哪种方式,当您读取类型为never的可选属性时,您肯定会得到一个undefined

So, receiving undefined is desired behavior.因此,接收undefined是期望的行为。


Anyway, the only way to get this kind of "check one property of an object to narrow the type of the whole object" functionality in TypeScript is to use a discriminated union .无论如何,在 TypeScript 中获得这种“检查对象的一个​​属性以缩小整个对象的类型”功能的唯一方法是使用可区分的 union If you want your content property to be a union and not a single object type, you need to distribute that property type across any union in T .如果你希望你content属性是工会,而不是一个单一的对象类型,则需要发布在任何联盟,物业类型T A distributive conditional type will do this automatically:分布式条件类型将自动执行此操作:

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
}

Which you can verify:您可以验证:

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;
} */

And thus the following assignment fails:因此以下分配失败:

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

And control flow analysis inside the implementation of getSuffixFromSession() proceeds as desired: getSuffixFromSession()实现中的控制流分析按需要进行:

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

Playground link to code Playground 链接到代码

You're almost correct, you just want to swap string for T .您几乎是正确的,您只想将string交换为T

Like this:像这样:

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 Playground TS游乐场

Since never would not display directly, you can replace the never with number , and you can see the following由于 never 不会直接显示,您可以将never替换为number ,您可以看到以下内容类型接口

I haven't dived into the TypeScript compiler source code, but I'm guessing that typescript is inferring T as 'b' | 'c'我还没有深入研究 TypeScript 编译器源代码,但我猜测 typescript 将T推断为'b' | 'c' 'b' | 'c' here, so the session is inferred to SessionDetails<'b' | 'c'> 'b' | 'c'在这里,所以session被推断为SessionDetails<'b' | 'c'> SessionDetails<'b' | 'c'> , eventually the session.content.suffix will be inferred to string | never | undefined SessionDetails<'b' | 'c'> ,最终session.content.suffix将被推断为string | never | undefined string | never | undefined

If you really want impl like this, maybe this is a better way, explicitly tell the TypeScript session is a SessionDetail<'c'>如果你真的想要这样的 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