Skip to content

Nested mapped types still evaluated eagerly in some cases, causing surprising/incorrect behavior #18089

Closed
@jcalz

Description

@jcalz

TypeScript Version: 2.6.0-dev.20170824 (not in 2.4 or 2.5)

(Moving from a comment to a new issue)

Now that the fix for #15756 is in (#18042), I tried implementing the type function Transpose, which reverses a mapping of string literals. Example:

// which kinds of ice cream does each person like
type IceCreamPreferences = {
  'alice': 'vanilla' | 'chocolate' | 'strawberry';
  'bob': 'chocolate';
  'carol': 'strawberry' | 'rumRaisin';
  'dave': 'rumRaisin' | 'chocolate';
  'eve': 'tripleFudgeRipple';
}
 
// which people like each kind of ice cream
type TransposedIceCreamPreferences = {
  'vanilla': 'alice';
  'chocolate': 'alice' | 'bob' | 'dave';
  'strawberry': 'alice' | 'carol';
  'rumRaisin': 'carol' | 'dave';
  'tripleFudgeRipple': 'eve';
}

Here is a version of Transpose that still doesn't work:

// union of possible value types
type ValueOf<T> = T[keyof T];

// subtract unions of string literals
type Diff<T extends string, U extends string> = (
  {[K in T]: K} &
  {[K in U]: never} &
  { [K: string]: never }
)[T];

type Transpose<T extends Record<string, string>> = ValueOf<{
  [P in keyof T]: Record<Diff<ValueOf<T>, T[P]>, never> & Record<T[P], P>
}> // broken!  

type WhoLikes = Transpose<IceCreamPreferences>;
var chocolateLover: WhoLikes['chocolate'];
chocolateLover = 'alice'; // okay
chocolateLover = 'bob'; // okay
chocolateLover = 'carol'; // 🙁 should error, but doesn't! 
chocolateLover = 'dave'; // okay
chocolateLover = 'eve'; // 🙁 should error, but doesn't! 

Expected behavior:
WhoLikes['chocolate'] should be 'alice' | 'bob' | 'dave'.

Actual behavior:
WhoLikes['chocolate'] is 'alice' | 'bob' | 'carol' | 'dave' | 'eve'. Something is still doing an eager substitution where I don't expect it.

I'm not sure if this is the minimal example of this issue; seems only to show up with a certain amount or level of nesting of mapped type definitions and applications. I think this is still a bug but I'm not sure. Thoughts?


Note that the default-generic workaround does work here:

type Transpose<T extends Record<string, string>, X = {
  [P in keyof T]: Record<Diff<ValueOf<T>, T[P]>, never> & Record<T[P], P>
}> = ValueOf<X> // works

type WhoLikes = Transpose<IceCreamPreferences>;
var chocolateLover: WhoLikes['chocolate'];
chocolateLover = 'alice'; // okay
chocolateLover = 'bob'; // okay
chocolateLover = 'carol'; // 🙂 error, carol doesn't like chocolate
chocolateLover = 'dave'; // okay
chocolateLover = 'eve'; // 🙂 error, eve doesn't like chocolate

so I do have a working Transpose in 2.6.0-dev.20170824, but I'm not sure if the workaround is necessary.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions