Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract not narrowing type union in mapped type passed as generic #57827

Open
mikeauclair opened this issue Mar 18, 2024 · 6 comments Β· May be fixed by #57838
Open

Extract not narrowing type union in mapped type passed as generic #57827

mikeauclair opened this issue Mar 18, 2024 · 6 comments Β· May be fixed by #57838
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@mikeauclair
Copy link

πŸ”Ž Search Terms

Extract, type union

πŸ•— Version & Regression Information

  • This changed between versions 5.33 and 5.4

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.4.2#code/C4TwDgpgBAysBOBLAdgcwNIRAZwDwBUA+KAXigFEAPBAQwGNhcBrLAewDMoBvAWACgogqAG0AClBRQWIDlHxQa2OWIC6UCNQjIAJkuwIUqKAH4oogFxRkEAG4R4KyzWQh+AXwA0UfUjSF+-HSsyPpQAEYANqysYKRQBB6EABQAXpZwvhhYeEQAlKTEvAJQQSHAUOyWPoZxKe4BfCjA9uz00Ira3PyCAI4A7hBVBmj1fPwREOU06cNZOLgd-nxAA

πŸ’» Code

type StringKeys<T> = Extract<keyof {
    [P in keyof T as T[P] extends string ? P: never]: any
}, string>

const bloop = <T,>(z: StringKeys<T>) => {
  const f: string = z
}

interface asd {
  qwe: string
}

let a: StringKeys<asd>

πŸ™ Actual behavior

z is not assignable to string in the bloop function

πŸ™‚ Expected behavior

z should be assignable to string, because we extract string-extending keys

Additional information about the issue

This was working just fine on 5.3.3, and continues to work when the generic is concretized (hence the let at the bottom of the example)

@Andarist
Copy link
Contributor

Andarist commented Mar 18, 2024

Conditional types are tricky as they often stay deferred in generic contexts. This version could somewhat easily work though (and it doesn't):

type StringKeys<T> = keyof {
  [P in keyof T as T[P] extends string ? P & string : never]: any;
};

const bloop = <T,>(z: StringKeys<T>) => {
  const f: string = z; // errors but shouldnt
};

As a workaround, you can use this version:

type StringKeys<T> = keyof {
  [P in keyof T as T[P] extends string ? P : never]: any;
} &
  string;

const bloop = <T,>(z: StringKeys<T>) => {
  const f: string = z;
};

@mikeauclair
Copy link
Author

That workaround works in my real codebase, so I'll cut over to that approach for now - is this a matter of the intersection (instead of Extract) causing non-deferred evaluation?

@fatcerberus
Copy link

fatcerberus commented Mar 18, 2024

I would guess it’s just a matter of the compiler knowing that T & string is always assignable to string regardless of what T is (by definition, as A & B is the set of values assignable to both)

@RyanCavanaugh
Copy link
Member

Bisects to #56742

@814k31
Copy link

814k31 commented Aug 6, 2024

Not sure if I should create my own issue but on topic for Extract not narrowing union with generics

typescript playground link

type Example = { case: '1', value: number } | { case: '2', value: string } | { case: '3', value: boolean }

function test<T extends Pick<Example, 'case'>>(field: T['case']): Extract<Example, T> {
    switch(field) {
        case '1': {
            return { case: '1', value: 1 };
        }
        case '2': {
            return { case: '2', value:  'string'};
        }
        case '3': {
            return { case: '3', value: true };
        }
        default:
            throw new Error()
    }
}

Gives the error:
image

Tried with a lot of versions (v5.5.4 and nightly for examples)

@harry0000
Copy link

harry0000 commented Sep 11, 2024

I'm not entirely sure if this is the same issue, but here is the code that is affected in my case. It results in a compilation error starting from 5.4.x.

const data = {
  1: { id: 1, name: "1" },
  2: { id: 2, name: "2" },
  3: { id: 3, name: "3" },
  4: { id: 4, name: "4" },
  5: { id: 5, name: "5" },
  6: { id: 6, name: "6" },
  7: { id: 7, name: "7" },
  8: { id: 8, name: "8" },
  9: { id: 9, name: "9" }
} as const;

type Ids = keyof typeof data
type Data = typeof data[keyof typeof data]

const someExtraData = {
  3: {}, 5: {}, 7: {}, 8: {}, 9: {}
} as const satisfies Partial<Record<Ids, {}>>;

type SomeIds = keyof typeof someExtraData
type SomeData<I extends SomeIds> = Extract<Data, { id: I }>

function foo<I extends SomeIds>(value: SomeData<I>) {
  // Property 'id' does not exist on type 'SomeExtraData<I>'.
  //   Property 'id' does not exist on type 'Extract<{ readonly id: 1; readonly name: "1"; }, { id: I; }>'.(2339)
  value.id;
}

playground

Update:

The workaround code for my case is as follows:

type SomeIds = keyof typeof someExtraData
type SomeData<I extends SomeIds> = Extract<typeof data[SomeIds], { id: I }>

function foo<I extends SomeIds>(value: SomeData<I>) {
  const id: I = value.id;
}

playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants