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

Exclude not work on [key: string]:any #31153

Closed
vipcxj opened this issue Apr 29, 2019 · 10 comments
Closed

Exclude not work on [key: string]:any #31153

vipcxj opened this issue Apr 29, 2019 · 10 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@vipcxj
Copy link

vipcxj commented Apr 29, 2019

TypeScript Version: 3.4.4

Search Terms:

Code

interface TestType {
   a: number;
   b: string;
   [key:string]: any;
}
type NTestType1 = {
   [P in keyof TestType]: any;
}
const test:NTestType1 = {} // error missing a and b. this is correct.
type NTestType2 = {
   [P in Exclude<keyof TestType, 'a'>]: any;
}
const test::NTestType2 = {} // no error, we expect show missing b error here.

Expected behavior:
show missing b error

Actual behavior:
no error

Playground Link:

Related Issues:

@jcalz
Copy link
Contributor

jcalz commented Apr 29, 2019

keyof on a type with a string index signature will always be at least as wide as string which will swallow up all string literals in a union. That is, string | "a" | "b" is collapsed to string. This is known behavior and not considered a bug. Excluding "a" from string is not something that can be done yet, although that might change.

There are ways of extracting the literal keys if you need to do it. I'd say this issue is a duplicate but I'm not sure which issue it duplicates. What issues do you think are related? Hmm, looks like you didn't actually search for it (since your "search terms" are empty). Oh well.

@vipcxj
Copy link
Author

vipcxj commented Apr 30, 2019

@jcalz Have you try your solution? No you didn't. it not work. Though it is the true solution to get a known key such as 'a' and 'b' in the example, this is not my finally target. My target is to produce a new type witch include key 'b' but not 'a' and any other unknown string key. In a word, I need a perfect Omit working on type with unknown properties.

type NTestType = Pick<Test, Exclude<KnownKeys<Test>, 'a'> | string>
const test::NTestType = {} // still no error

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Apr 30, 2019

@vipcxj Let's keep discussions polite and constructive. @jcalz was giving you the starting point for a solution, not a complete solution. KnownKeys is necessary to craft a solution, but Exclude<KnownKeys<Test>, 'a'> | string will result in the same issue that string will eat up any literal types you put next to it (those coming from KnownKeys)

To actually get it to work, you first have to Pick the known keys from the type and then add the index back in. A generic solution (although not extensively tested) could look something like this:

type KnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends {[_ in keyof T]: infer U} ? U : never;

interface TestType {
   a: number;
   b: string;
   [key:string]: any;
}

// A bit of arm wrestling to convince TS  KnownKeys<T> is keyof T
type OmitFromKnownKeys<T, K extends PropertyKey> = KnownKeys<T> extends infer U ? 
    [U] extends [keyof T] ? Pick<T, Exclude<U, K>>:
    never : never;
type OmitOnIndexed<T, K extends PropertyKey> = 
    OmitFromKnownKeys<T, K> & // Get the known part without K
    (string extends keyof T ? { [n: string]: T[keyof T]} : {}) // Add the index signature back if necessary

const test:OmitOnIndexed<TestType, 'a'> = { } // error

Note The solution above might not work as expected once we get any type in index signatures. But we can deal with that when it ships :)

@vipcxj
Copy link
Author

vipcxj commented Apr 30, 2019

@dragomirtitian thanks, it partially works. I don't know whether it is the most perfect Omit version, it is the most perfect Omit version I have seen.
This is my updated full version:

type KnownKeys<T> = {
    [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? ({} extends U ? never : U) : never; // I don't know why not just U work here, but ({} extends U ? never : U) work
type OmitFromKnownKeys<T, K extends keyof T> = KnownKeys<T> extends infer U ? 
    [U] extends [keyof T] ? Pick<T, Exclude<U, K>>:
    never : never;
type Omit<T, K extends keyof T> = OmitFromKnownKeys<T, K>
  & (string extends K ? {} : (string extends keyof T ? { [n: string]: T[Exclude<keyof T, number>]} : {})) // support number property
  & (number extends K ? {} : (number extends keyof T ? { [n: number]: T[Exclude<keyof T, string>]} : {})) // support number property

It is very very complicate. and I still not sure it is the perfect version. I think the perfect version Omit function should be built in.

@dragomirtitian
Copy link
Contributor

@vipcxj Typescript tends to offer the building blocks to build such types, they also tend not to include very complicated types in the base lib (the relatively simple Omit just made it in recently)

Your version includes number index which makes the solution more complete. The problem is that we will need to do some even weirder types when #26797 drops and the index can be any type not just number or string.

The root problem is that there isn't a good way to get the index signature out of a type and to differentiate known keys from the index signature. While this solution works, I do believe it might break in the future (not necessarily break for existing code, but not work with new features). This is also a very good point against including it in the base lib

@vipcxj
Copy link
Author

vipcxj commented Apr 30, 2019

@dragomirtitian what I mean is not including it in lib but include it in the syntax level or some native level. Omit is the very very often used type, and it even tightly bind to some syntax, such as the following example. But I haven't seen such a compete version before, even the official version is incomplete.

interface TestType {
   a: number;
   b: string;
   [key:string]: any;
}
const test: TestType = { a: 1, b: 'test', c: {}, d: 1 }
const { a, ...rest } = test // it seems that typescript think the type of rest is {}. and auto complete hints of rest show nothing. Its type should be Omit<TestType, 'a'>. this should be a very base feature.

@felschr
Copy link

felschr commented Jul 26, 2021

As reported in #44143 typescript 4.3 broke the KnownKeys type which also breaks the OmitFromKnownKeys implementations posted here.
In the above issue there's a new KnownKeys implementation which works in typescript 4.3 but OmitFromKnownKeys remains broken.

I've tried fixing its implementation and I came up with a solution with a small caveat, though:

type KnownKeys<T> = {
  [K in keyof T as string extends K
  ? never
  : number extends K
  ? never
  : K]: T[K];
};

type OmitFromKnownKeys<T, K extends keyof T> = KnownKeys<T> extends infer U ?
  keyof U extends keyof T
  ? Pick<T, Exclude<keyof U, K>> & Pick<T, Exclude<keyof T, keyof KnownKeys<T>>>
  : never : never;

While this is generally working, it resolves to a type like Pick<Test, "b" | "c"> & Pick<Test, string | number> instead of an inferred object type, which most would likely prefer:

type Test = {
  [x: number]: any,
  [y: string]: any,
  a: number,
  b: string,
  c: string,
}

// resolves to:
// const test: Pick<Test, "b" | "c"> & Pick<Test, string | number>
// instead of:
// const test: {
//     [x: string]: any;
//     [x: number]: any;
//     a: string;
// }
const test: OmitFromKnownKeys<Test, "b" | "c"> = {
  a: 42,
  [0]: 21,
  ["something"]: "test",
}

Does anyone have an idea for how to make this resolve to the object type instead without losing the known keys?

@augustobmoura
Copy link

augustobmoura commented Mar 21, 2022

Just a friendly bump, because I found a similar situation in my code. I also found that you can remove the index signature (as shown here), Omit the properties, and then re-add the signature back, like so:

// Returns a type with the same well-known props, but without the index signature
type NoIndex<T> = {
  [K in keyof T as {} extends Record<K, 1> ? never : K]: T[K]
}

// Inverse of NoIndex, meaning it only gets the index signature of a type, ignoring other well known props
type OnlyIndex<T> = {
  [K in keyof T as {} extends Record<K, 1> ? K : never]: T[K]
}

// Omit from NoIndex version of T, and then intersects with the indexed part of it
type OmitFromKnownKeys<T, K extends keyof NoIndex<T>> = Omit<NoIndex<T>, K> & OnlyIndex<T>

type type Test = {
  [x: number]: any,
  [y: string]: any,
  a: number,
  b: string,
  c: string,
}

type TestOmitted = OmitFromKnownKeys<Test, 'b' | 'c'>

@whzx5byb
Copy link

whzx5byb commented Sep 5, 2022

Does anyone have an idea for how to make this resolve to the object type instead without losing the known keys?

@felschr You could use Any.Compute util type from https://github.com/millsp/ts-toolbelt to provide a better display.

I have extracted the code there and made an example, based on #31153 (comment) by @augustobmoura.

@yepitschunked
Copy link

yepitschunked commented Feb 13, 2024

Edit: looks like this is the same suggestion as #54451!

I stumbled upon this simple type which I think solves the Omit problem in this thread:

type OmitFromMappedType<Type, ToOmit> = {
  [Property in keyof Type as Exclude<Property, ToOmit>]: Type[Property];
};

As @jcalz mentioned, trying to separate out the "known" keys from the union is really hard. But we can dodge this problem altogether. Using key remapping, we can map over and Exclude the individual keys of Type, instead of passing the whole keyof union to Exclude. We never run into the union collapse issue since we're considering each key individually.

I made an example based on @whzx5byb's example above.

This doesn't really solve the Exclude problem that started the thread, but I think the Omit use case is why people want it anyway. Should also be robust to the issues @dragomirtitian raised since we're not making any assumptions about the types of property keys, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants