Type queries allow us to obtain type information from values, which is an incredibly important capability — particularly when working with libraries that may not expose type information in a way that’s most useful for you
keyof
The keyof type query allows us to obtain type representing all property keys on a given interface
tsTrytypeDatePropertyNames = keyofDate
Not all keys are strings, so we can separate out those keys that are symbols and those that are strings using the intersection operator (&).
If you remember your geometry, it may be useful to think of this as kind of like a dot product, in that when we use the intersection operator, we’re left only with the sub-part of the keyof Date that also is included by string or symbol, respectively.
tsTrytypeDatePropertyNames = keyofDate typeDateStringPropertyNames =DatePropertyNames & stringtypeDateSymbolPropertyNames =DatePropertyNames & symbol
Interesting! this Symbol.toPrimitive property is the only non-string. 1
typeof
The typeof type query allows you to extract a type from a value. An example is shown below
tsTryasync functionmain () {constapiResponse = awaitPromise .all ([fetch ("https://example.com"),Promise .resolve ("Titanium White"),])typeApiResponseType = typeofapiResponse }
A common use of typeof is to obtain a type representing the “static site” of a class (meaning: constructor, static properties, and other things not present on an instance of the class)
tsTryconstMyAjaxConstructor =CSSRule CSSRule .STYLE_RULE constmyAjax = newCSSRule ()
MyAjaxConstructor, the class (constructor) is of type typeof CSSRule, where instances are of type CSSRule
Indexed Access Types
Indexed Access types provide a mechanism for retrieving part(s) of an array or object type via indices. We’ll look at how this kind of type works, and a couple of practical examples of where you might use them.
At the simplest level, these kinds of types are all about accessing some part of another type, via an index
tsTryinterfaceCar {make : stringmodel : stringyear : numbercolor : {red : stringgreen : stringblue : string}}letcarColor :Car ["color"]
In this situation 'color' is the “index”.
The index you use must be a valid “key” you could use on a value of type Car. Below you can see what happens if you try to break this rule:
tsTryletProperty 'not-something-on-car' does not exist on type 'Car'.2339Property 'not-something-on-car' does not exist on type 'Car'.carColor :Car ["not-something-on-car" ]
You can also reach deeper into the object through multiple “accesses”
tsTryletcarColorRedComponent :Car ["color"]["red"]
…and you can pass or “project” a union type (|) through Car as an index, as long as all parts of the union type are each a valid index
tsTryletcarProperty :Car ["color" | "year"]
Use case: the “type registry” pattern
We’re going to touch on one concept we haven’t talked about yet, but we can use a basic definition for the purpose of understanding this example.
tsdeclare module "./lib/registry" {}
This is called a module declaration, and it allows us to effectively layer types on top of things already exported by a module ./lib/registry.ts. Remember, there’s only one definition of the types exported by ./lib/registry.ts, so if we modify them using a module declaration, that modification will affect every place where its types are used.
Now, let’s use keyof, module declarations and what we just learned about open interfaces to solve a problem.
Imagine we’re building a data library for a web applications. Part of this task involves building a function that fetches different types of records from a user’s API. We want to be able to retrieve a record by the name of the kind of record and its ID, but as the builders of the library, we don’t know the specific types that any given user will need.
ts// Assumption -- our user has set up resources like Book and Magazine//// returns a BookfetchRecord("book", "bk_123")// returns a MagazinefetchRecord("magazine", "mz_456")// maybe should refuse to compilefetchRecord("blah", "")
Our project might have a file structure like
jsdata/book.ts // A model for Book recordsmagazine.ts // A model for Magazine recordslib/registry.ts // Our type registry, and a `fetchRecord` functionindex.ts // Entry point
Let’s focus on that first argument of the fetchRecord function. We can create a “registry” interface that any consumer of this library can use to “install” their resource types, and define the fetchRecord function using our new keyof type query.
tsTry// @filename: lib/registry.tsexport interfaceDataTypeRegistry {// empty by design}// the "& string" is just a trick to get// a nicer tooltip to show you in the next stepexport functionfetchRecord (arg : keyofDataTypeRegistry & string,id : string) {}
Now let’s focus our attention toward “app code”. We’ll define classes for Book and Magazine and “register” them with the DataTypeRegistry interface
tsTry// @filename: data/book.tsexport classBook {deweyDecimalNumber (): number {return 42}}declare module "../lib/registry" {export interfaceDataTypeRegistry {book :Book }}// @filename: data/magazine.tsexport classMagazine {issueNumber (): number {return 42}}declare module "../lib/registry" {export interfaceDataTypeRegistry {magazine :Magazine }}
Now look what happens to the first argument of that fetchRecord function! it’s "book" | "magazine" despite the library having absolutely nothing in its code that refers to these concepts by name!
tsTry// @filename: index.tsimport {DataTypeRegistry ,fetchRecord } from './lib/registry'fetchRecord ("book", "bk_123")
Obviously there are other things we’d need to build other parts of what we’d need for a fetchRecord function. Don’t worry! We’ll come back once we’ve learned a few more things that we need!
-
If you’re curious about this property, try running the following in your terminal
↩node -e "console.log(new Date()[Symbol.toPrimitive]('string'))"