I was trying to get deeper understanding of advanced types in Typescript one of those types as example is NonFunctionPropertyNames which extracts only a properties of a given object.
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
I can understand the first part inside the curly brackets '{ [K in keyof T]: T[K] extends Function ? never : K }', we are declaring an object and exclude the properties which extend Function. What puzzles me is the part after the curly brackets [keyof T]. This looks like a definition of an array {...}[keyof T] but it in fact returns object.
Can someone explain why only the curly brackets part is not enough for declaring the type, and what is the role of [keyof T].
What you are seeing there is a type query. If you index into an object type you get the type of that property. For example:
type Foo = { foo: number }['foo'] // is number
If you are indexing using a union of several properties you get a union of all the property types:
type FooBar = { foo: number, bar: string, baz: boolean }['foo' | 'bar'] // string | number
If you index using all keys you get the a union of all property types:
type FooBarBaz = { foo: number, bar: string, baz: boolean }['foo' | 'bar' | 'baz'] // string | number | boolean
But to get a union of all property names you can use keyof so the above type can also be written as:
type O = { foo: number, bar: string, baz: boolean }
type FooBarBaz = O[keyof O] // string | number | boolean
The type { [K in keyof T]: K } evaluates to an object type, where the keys are typed as the same literal type representing the key:
type O = { foo: number, bar: string, baz: boolean }
type FooBarBaz = { [K in keyof O]: K } // { foo: "foo"; bar: "bar"; baz: "baz"; }
What the conditional type does is make some of those keys, not the same type as the literal type represeting the key, but types them as never instead:
type O = { foo: number, bar: string, baz: () => boolean } // baz is a function now
type NonFunctionPropertyNames = { [K in keyof O]: O[K] extends Function ? never: K } // { foo: "foo"; bar: "bar"; baz: never; }
So the new type still has all the keys of the original, but some are typed as the literal type of the corresponding key, and some are typed as never. What we want is a union with all the value types of the keys in the type we just constructed, and we can use keyof O as before (since the type has the same keys as O):
type O = { foo: number, bar: string, baz: () => boolean } // baz is a function now
type NonFunctionPropertyNames = { [K in keyof O]: O[K] extends Function ? never: K }[keyof O] // "foo" | "bar" | never = "foo" | "bar" ;
never is always removed from unions, so we get at the end a union of just those object keys that were not never.
Make O a type parameter and you have a reusable type to get the non function keys:
type NonFunctionPropertyNames<O> = { [K in keyof O]: O[K] extends Function ? never : K }[keyof O]
type Foo = { foo: number, bar: string, baz: () => boolean } // baz is a function now
type NonFUnctionKeysOfFoo = NonFunctionPropertyNames<Foo> // "foo" | "bar"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With