Typescript: Exclude not work on [key: string]:any

Created on 29 Apr 2019  路  6Comments  路  Source: microsoft/TypeScript


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:

Question

Most helpful comment

@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 :)

All 6 comments

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.

@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

@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 :)

@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.

@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

@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.
Was this page helpful?
0 / 5 - 0 ratings

Related issues

uber5001 picture uber5001  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

dlaberge picture dlaberge  路  3Comments

manekinekko picture manekinekko  路  3Comments

blendsdk picture blendsdk  路  3Comments