最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

TypeScript invariance and structural type checking on nested generics strange behavior - Stack Overflow

programmeradmin1浏览0评论

I've encountered strange behavior when using complex nested generics.

Here is the distilled version of my real-life problem:

type Variable = 'A' | 'B';

type Option<T extends Variable> = T extends any ? { value: T } : never;

type OptionAny = Option<Variable>;

type Generic<T extends Variable> = Extract<OptionAny, { value: T }>;

// ==============================================================================
// JUST IN CASE, IF YOU'RE CURIOUS, THIS IS FINE:
type GenericHandler<T extends Generic<Variable>> = { handle(input: T): void };
// no error
type GenericHandlerA = GenericHandler<Generic<'A'>>;
// ==============================================================================

type NestedGeneric<T extends Variable> = { generic: Generic<T> };

type NestedGenericHandler<T extends NestedGeneric<Variable>> = {
  handle(input: T): void;
};

// error
//  TS2344: Type NestedGeneric<'A'> does not satisfy the constraint NestedGeneric<Variable>
//    Type Variable is not assignable to type 'A'
//      Type 'B' is not assignable to type 'A'
type NestedGenericHandlerA = NestedGenericHandler<NestedGeneric<'A'>>;

// -----------------------------------------------------------------------------
// the expected solution (to my understanding of TS):
// use distributive conditional type on type, that is nesting another generic
// -----------------------------------------------------------------------------

// distribute over possibly union T
type NestedGenericDistributive<T extends Variable> = T extends any
  ? { generic: Generic<T> }
  : null;

type NestedGenericDistributiveHandler<
  T extends NestedGenericDistributive<Variable>
> = {
  handle(input: T): void;
};

// ok
type NestedGenericDistributiveHandlerA = NestedGenericDistributiveHandler<
  NestedGenericDistributive<'A'>
>;

// ok, too
type NestedGenericDistributiveHandlerA2 = NestedGenericDistributiveHandler<
  NestedGeneric<'A'>
>;

// because:
type TestDistributive = NestedGenericDistributive<Variable>;
//   ^? {generic: Generic<"A">} | {generic: Generic<"B">}

type TestNonDistributive = NestedGeneric<Variable>;
//   ^? {generic: Generic<"A" | "B">}

// -----------------------------------------------------------------------------
// Now, the bloody black magic:
// Use type aliases for either the constraint or concrete NestedGeneric type
// -----------------------------------------------------------------------------

type NestedGenericAliasAny = NestedGeneric<Variable>;
type NestedGenericAliasHandler<T extends NestedGenericAliasAny> = {
  handle(input: T): void;
};
// no error
type NestedGenericAliasHandlerA = NestedGenericAliasHandler<NestedGeneric<'A'>>;

// -----------------------------------------------------------------------------

type NestedGenericAAlias = NestedGeneric<'A'>;
// no error
type NestedGenericHandlerAAlias = NestedGenericHandler<NestedGenericAAlias>;

// -----------------------------------------------------------------------------
// More bloody magic...
// Replacing `Generic` with:
// -----------------------------------------------------------------------------

// doesn't help
type GenericDistributiveBad<T extends Variable> = T extends any
  ? Extract<OptionAny, { value: T }>
  : null;

// even though
type TestBad = GenericDistributiveBad<Variable>;
//   ^? | Extract<{value: "A"} | {value: "B"}, {value: "A"}>
//      | Extract<{value: "A"} | {value: "B"}, {value: "B"}>

// But this does help
type GenericDistributiveGood<T extends Variable> =
  | (T extends 'A' ? { value: T } : never)
  | (T extends 'B' ? { value: T } : never);

// Is this different to `TestBad`?
type TestGood = GenericDistributiveGood<Variable>;
//   ^? {value: "A"} | {value: "B"}

// afterall, `Extract` is
//    `type Extract<T, U> = T extends U ? T : never;`
// which means that each type in the TestBad union must go through the following:
//  Extract<{value: "A"} | {value: "B"}, {value: "A"}>
//    is equal to
//      | ({value: "A"} extends {value: "A"} ? {value: "A"} : never)
//      | ({value: "B"} extends {value: "A"} ? {value: "B"} : never)
// same for Extract<{value: "A"} | {value: "B"}, {value: "B"}>
//  ...
// Result is: {value: "A"} | {value: "B"}

// -----------------------------------------------------------------------------
// Oh, and BTW
// -----------------------------------------------------------------------------

type TestGeneric = Generic<'A' | 'B'>;
//   ^? {value: "A"} | {value: "B"}

// :)

Can anybody explain strictly how it works and why?

(Goodluck answering this with ChatGPT)


To my understanding typescript should just try to check if NestedGeneric<'A'> is assignable to constraint NestedGeneric<Variable>

While doing it, it should check if type parameter T in NestedType is invariant or covariant (correct me if I am messing up the terminology). If the NestedType is a distributive conditional type over T, the T is covariant, otherwise, by default, it is invariant. If it is invariant, and the given type (of T) is not exactly the same as the constraint, ts throws an error.

Otherwise, it goes deeper.

Therefore

type NestedGenericHandlerA = NestedGenericHandler<NestedGeneric<'A'>>;

raises TS error, and the "expected solution" works

type NestedGenericDistributive<T extends Variable> = T extends any
  ? { generic: Generic<T> }
  : null;

this is covariant case and everything is ok.

So, the first question is - is the above explanation correct?


Now, if it is correct, then what is going on in the code that goes below the "expected solution"?

发布评论

评论列表(0)

  1. 暂无评论