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

Should mapped types where the result is never filter keys? #23199

Closed
Pajn opened this issue Apr 6, 2018 · 7 comments
Closed

Should mapped types where the result is never filter keys? #23199

Pajn opened this issue Apr 6, 2018 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@Pajn
Copy link

Pajn commented Apr 6, 2018

I am not sure that this is a bug but it did behave in a way that I did not expect which is why I filed a new issue. If this is not a bug it should probably be closed and continued in earlier issues like #17678

TypeScript Version: 2.8.1

Search Terms:
Mapped types, never, filter, conditional types, Exclude, Omit, Pick

Code

type Fruit = { name: string };
type Apple = Fruit & { cultivar: string };

type Fruits = {
  banana: Fruit;
  alice: Apple;
  gala: Apple;
};

type Apples = {
  [T in keyof Fruits]: Fruits[T] extends Apple ? Fruits[T] : never
};

Expected behavior:
I expected Apples to have type

{
  alice: Apple;
  gala: Apple;
}

Actual behavior:
Apples have the type

{
  banana: never;
  alice: Apple;
  gala: Apple;
}

Having an object with a never key is quite strange to me. How would that object ever be created?

Playground Link: http://www.typescriptlang.org/play/#src=%0D%0Atype%20Fruit%20%3D%20%7B%20name%3A%20string%20%7D%3B%0D%0Atype%20Apple%20%3D%20Fruit%20%26%20%7B%20cultivar%3A%20string%20%7D%3B%0D%0A%0D%0Atype%20Fruits%20%3D%20%7B%0D%0A%20%20banana%3A%20Fruit%3B%0D%0A%20%20alice%3A%20Apple%3B%0D%0A%20%20gala%3A%20Apple%3B%0D%0A%7D%3B%0D%0A%0D%0Atype%20Apples%20%3D%20%7B%0D%0A%20%20%5BT%20in%20keyof%20Fruits%5D%3A%20Fruits%5BT%5D%20extends%20Apple%20%3F%20Fruits%5BT%5D%20%3A%20never%0D%0A%7D%3B%0D%0A

Related Issues:
If this have worked as expected #17678 would be already be supported. That issue is marked as a duplicate and linked to #12424 and #17636 but I am not sure I see the direct relation there to be honest.

@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Apr 6, 2018
@mhegazy
Copy link
Contributor

mhegazy commented Apr 6, 2018

Technically a type with at least one property of type never is itself never; since there is no run time value that can satisfy that type, and this is it the empty type (i.e. never).

that said, the way the compiler defers getting the types of properties. this is the only way really to reason about structural types and recursive types without chocking. so when an type is created, the compiler only knows about its properties, and not their types. it only compute the property types when it needs to. needing to do this filtering means that when a type is created the compiler needs to figure out its property types to know whether to add the property or not.

You can achieve what you were looking for by first filtering the keys, then creating a mapped type from the filtered keys.. e.g.:

type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];

type Apples = {
    [Q in FilteredKeys<Fruits, Apple>]: Fruits[Q]
};

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@ulrichb
Copy link

ulrichb commented May 4, 2018

You can achieve what you were looking for by first filtering the keys, then creating a mapped type from the filtered keys.. e.g.:

Nice! Has the same issue and this solves it. Thanks!

@alexfoxgill
Copy link

alexfoxgill commented Jul 26, 2018

unfortunately this does not seem to preserve optional members:

type SubtractKeys<T, U> = { [K in keyof T]: K extends keyof U ? never : K }[keyof T]
type Subtract<T, U> = { [K in SubtractKeys<T, U>]: T[K] }

type Foo = {
  a: string
  b?: string
}

type Bar = {
  a: string
}

type FooWithoutBar = Subtract<Foo, Bar>
// {
//   b: string | undefined
// }

there is a difference between { b: string | undefined } and { b?: string } - in the former case you can't just omit the property, you have to specify it as undefined

@kresli
Copy link

kresli commented Mar 23, 2019

also it doesn't work with generics at all

interface ISimpleString {
    value: string;
}
interface ISimpleNumber{
    value: number;
}

interface ModelPropertiesDeclaration {
    [key: string]: ISimpleString
}

type IModelType<P extends ModelPropertiesDeclaration> = {}

type FilteredKeys<T, U> = { [P in keyof T]: T[P] extends U ? P : never }[keyof T];

// =====
// Test
// =====

interface TestModel {
    label: ISimpleString,
    age: ISimpleNumber
};

type BuildFromPropsC = IModelType<{
    [K in FilteredKeys<TestModel, ISimpleString>]: TestModel[K] // <-- this works
}>

type BuildFromProps<P extends TestModel> = IModelType<{
    [K in FilteredKeys<P, ISimpleString>]: P[K] // <-- doesn't work
}>;

playground
https://www.typescriptlang.org/play/#src=%0D%0Ainterface%20ISimpleString%20%7B%0D%0A%20%20%20%20value%3A%20string%3B%0D%0A%7D%0D%0Ainterface%20ISimpleNumber%7B%0D%0A%20%20%20%20value%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20ModelPropertiesDeclaration%20%7B%0D%0A%20%20%20%20%5Bkey%3A%20string%5D%3A%20ISimpleString%0D%0A%7D%0D%0A%0D%0Atype%20IModelType%3CP%20extends%20ModelPropertiesDeclaration%3E%20%3D%20%7B%7D%0D%0A%0D%0Atype%20FilteredKeys%3CT%2C%20U%3E%20%3D%20%7B%20%5BP%20in%20keyof%20T%5D%3A%20T%5BP%5D%20extends%20U%20%3F%20P%20%3A%20never%20%7D%5Bkeyof%20T%5D%3B%0D%0A%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%0D%0A%2F%2F%20Test%0D%0A%2F%2F%20%3D%3D%3D%3D%3D%0D%0A%0D%0Ainterface%20TestModel%20%7B%0D%0A%20%20%20%20label%3A%20ISimpleString%2C%0D%0A%20%20%20%20age%3A%20ISimpleNumber%0D%0A%7D%3B%0D%0A%0D%0Atype%20BuildFromPropsC%20%3D%20IModelType%3C%7B%0D%0A%20%20%20%20%5BK%20in%20FilteredKeys%3CTestModel%2C%20ISimpleString%3E%5D%3A%20TestModel%5BK%5D%20%2F%2F%20%3C--%20this%20works%0D%0A%7D%3E%0D%0A%0D%0Atype%20BuildFromProps%3CP%20extends%20TestModel%3E%20%3D%20IModelType%3C%7B%0D%0A%20%20%20%20%5BK%20in%20FilteredKeys%3CP%2C%20ISimpleString%3E%5D%3A%20P%5BK%5D%20%2F%2F%20%3C--%20doesn't%20work%0D%0A%7D%3E%3B%0D%0A%0D%0A

@simeyla
Copy link

simeyla commented Aug 14, 2021

TypeScript 4.1 - Key Remapping

Typescript 4.1 has a new feature Key Remapping.

You can now do the following:

type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            This is the new syntax!
}

And yes, if you produce never (for the key) the property will be filtered out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

8 participants