I have an object which contains some predefined data for my application, which is stored in a const variable like this:
const data:{[key:string]:any} =Object.freeze({
some: 123,
long: {"a":"b"},
list: ["c"],
of: "",
arbitrary: null,
things: 1.2,
});
The keys of this object are known to the rest of the application. Consider this function which accesses the data object:
function doWork(k) {
if(!data.hasOwnProperty(k)) throw Error();
let value = data[k];
//...
}
This is called with strings like
doWork("things");
I would like to replace that runtime error for invalid keys with a Typescript compile-time check. I would like to be able to write
function doWork(k: keyof data) {
let value = data[k];
//...
}
But apparently the keyof
operator does not work that way. I get an error TS2304: Cannot find name 'data'.
My workaround: I can extract the keys of the object with something like this:
console.log("\""+Object.keys(data).join("\"|\"")+"\"");
Which I can then copy/paste and define as a type.
type data_key = "some"|"long"|"list"|"of"|"arbitrary"|"things"
export function doWork(k:data_key) {
let value = data[k];
//...
}
This feels like a silly hack and is quite inconvenient whenever I have to make a change, because I have to remember to put in this statement at the right place, run the program, and manually copy in the values back into the source code (or realistically, just type in the changes myself).
I am open to a better solution. Is there a language feature that provides the functionality I am looking for?
The string literal type allows you to specify a set of possible string values for a variable, only those string values can be assigned to a variable. TypeScript throws a compile-time error if one tries to assign a value to the variable that isn't defined by the string literal type.
In TypeScript, object is the type of all non-primitive values (primitive values are undefined , null , booleans, numbers, bigints, strings). With this type, we can't access any properties of a value.
object is a type that represents the non-primitive type, i.e. anything that is not number , string , boolean , bigint , symbol , null , or undefined . Argument of type 'undefined' is not assignable to parameter of type 'object | null'.Argument of type 'undefined' is not assignable to parameter of type 'object | null'.
Let TypeScript infer the type of data
, then extract the keys from the type it infers by using type data_key = keyof typeof data;
:
const data = Object.freeze({
some: 123,
long: {"a":"b"},
list: ["c"],
of: "",
arbitrary: null,
things: 1.2,
});
type data_key = keyof typeof data;
function doWork(k: data_key) {
let value = data[k];
//...
}
On the playground.
How that works:
Object.freeze
with the keys some
, long
, list
, etc. Object.freeze
is defined as freeze<T>(o: T): Readonly<T>
¹ so it returns a Readonly
version of that same inferred type.keyof
gets the keys of a type.typeof
in this context is TypeScript's typeof
, not JavaScript's. In TypeScript, there are places it expects runtime value, and other places it expects compile-time types. keyof
's operand is a place where a compile-time type is expected, so typeof data
returns the compile-time type of data
, which is the Readonly version of the inferred type from freeze
(with the keys some
, long
, list
, etc.). (If you had x = typeof data
, that would be typeof
in a context where a runtime value is expected, so it would be JavaScript's typeof
, and x
would get the runtime value "object"
.)¹ There are actually three definitions of freeze
in lib.es5.d.ts
(one for arrays, one for functions, and one for all other kinds of objects; this is that last one).
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