Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript Advanced Type NonFunctionPropertyNames Explained

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

like image 605
Hivaga Avatar asked Jul 03 '26 15:07

Hivaga


1 Answers

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"
like image 109
Titian Cernicova-Dragomir Avatar answered Jul 07 '26 10:07

Titian Cernicova-Dragomir