Get Javascript interface keys as string (like an Enum) - javascript

My request is actually really simple but I have found almost nothing of the sort online. Picture this, you have an interface like so:
interface MyInterface: {
propertyA: number;
propertyB: string;
propertyC: boolean;
}
Now you want to use the name of a field to access its value dynamically so you do something like object['propertyA'], where object is of type MyInterface.
However, passing 'propertyA' as a raw string is impractical and dangerous because if you rename it or change the interface the code won't work. (And it doesn't get picked up by typescript but I'm not sure this is possible anyway).
Ideally, I would like to do something like this MyInterface.propertyA, I was saddened that this doesn't seem to exist, I don't see any reason why it shouldn't.
And now before you say that's what object.propertyA is for, I have simplified the concept to make it easy to understand but the use case is not exactly this. I might pass fields of an interface as strings from react components within the JSX to make use of a reusable method or I could be checking whether a string matches a subset of keys in an interface to display errors accordingly.
Autogenerating an enum from the interface/class is not a bad solution although not ideal as it still incurs an overhead but as long as it doesn't need to be updated each time the interface changes and it doesn't depend on the type of the interface's properties then that's not bad.

Interfaces don't exist at runtime, so there's no way to get the property names of an interface as runtime values. What you can do, though, is go the other way: Have a "model" object:
const myInterfaceModel = {
propertyA: 42,
propertyB: "x",
propertyC: true,
};
Then define the interface using that object:
type MyInterface = typeof myInterfaceModel;
That defines MyInterface just as it is in your question.
Then you can create an object that has the property names as strings, like this:
const myInterfaceKeys = Object.fromEntries(
Object.keys(myInterfaceModel).map((key) => [key, key])
);
Now, if we just did that, myInterfaceKeys would be (effectively) Record<string, string>, which isn't useful for devex. But we can do better by using a mapped type on it:
type KeyNames<T> = {
[Key in keyof T]: Key;
};
// ...
const myInterfaceKeys = Object.fromEntries(
Object.keys(myInterfaceModel).map((key) => [key, key])
) as KeyNames<MyInterface>;
Now, its type matches its runtime value, and we get useful autocompletion:
Example usage (just a basic function, but could as easily be a React component):
function example(obj: MyInterface, key: keyof MyInterface) {
console.log(obj[key]);
}
declare let exampleObject: MyInterface;
example(exampleObject, myInterfaceKeys.propertyA);
Here's that all together (playground link):
type KeyNames<T> = {
[Key in keyof T]: Key;
};
const myInterfaceModel = {
propertyA: 42,
propertyB: "x",
propertyC: true,
};
type MyInterface = typeof myInterfaceModel;
// ^? type MyInterface = { propertyA: number, propertyB: string; propertyC : boolean }
const myInterfaceKeys = Object.fromEntries(
Object.keys(myInterfaceModel).map((key) => [key, key])
) as KeyNames<MyInterface>;
myInterfaceKeys
// ^−−−− Type a period after this to see the autocompletion
Usually I'd just use string literals (example(exampleObject, "propertyA")) since the IDE will offer the appropriate choices, and since keyof MyInterface is restrictive, if you remove a property, any code that referenced it with a string literal would stop compiling. But if you want that object with the keys on it, you can do the above to achieve that. (Also, I just tried, and if you use your object of keys, renaming a property works throughout, whereas the string literals didn't.)

Are you looking for something like this?
enum Direction {
Up = "u",
Down = "d",
Left = "l",
Right = "r",
}
interface MyDirection {
[Direction.Up]:string;
[Direction.Down]:number;
}
const obj:MyDirection = {
[Direction.Up]: 'up',
[Direction.Down]:12,
}
console.log(obj[Direction.Down], Direction.Down)

Related

Best way to declare string structure with TypesScript?

Let say I have this JavaScript object:
const myObject = {
id: 'my_id', // Base value that following value's structures will be depended on (always pascal case).
upperID: 'MY_ID', // Uppercase of `id`.
camelID: 'myId', // Camel case of `id`.
}
I want to use TypeScript to ensure that id is always pascal lower case. The upperID and camelID has same "value" with different string structure as above. What would be the best way to declare this myObject type in TypeScript?
The upper case one is pretty easy with the provided Uppercase<T> utility type.
type CasedIds<ID extends string> = {
id: ID,
upperID: Uppercase<ID>
}
const myObject: CasedIds<'my_id'> = {
id: 'my_id',
upperID: 'MY_ID',
} as const
See playground
The camel case one gets tricky. So first you need a type that can do this to a string.
There's probably a few ways to do this. Here's one.
type CamelCase<T extends string> =
T extends `${infer Pre}_${infer Letter}${infer Post}`
? `${Pre}${Capitalize<Letter>}${CamelCase<Post>}`
: T
type TestCamel = CamelCase<'abc_def_ghi'> // 'abcDefGhi'
See playground
Let's walk through this.
This generic type takes a string type as the generic parameter T. It then check to see if T extends a string of a certain pattern that contains an underscore followed by some characters.
If it does, then infer some substrings from that pattern. Infer the part before the underscore as Pre, the character after the underscore as Letter and the rest of the string as Post. Else, just use the string type as is.
Then we can make a new string from the Pre the Letter capitalized, and the Post. But there maybe more underscores, so we do the whole thing again on Post. This is a recursive type that stops recursing when there are no underscores left.
Using that the rest is easy:
type CamelCase<T extends string> =
T extends `${infer Pre}_${infer Letter}${infer Post}`
? `${Pre}${Capitalize<Letter>}${CamelCase<Post>}`
: T
type CasedIds<ID extends string> = {
id: ID,
upperID: Uppercase<ID>
camelID: CamelCase<ID>
}
const myObject: CasedIds<'my_id'> = {
id: 'my_id',
upperID: 'MY_ID',
camelID: 'myId',
} as const
See playground
Although you'll probably want a function build these for you:
function makeId<T extends string>(id: T): CasedIds<T> {
return {} as unknown as CasedIds<T> // mock a real implementation
}
const myId = makeId('my_id')
myId.id // type: 'my_id'
myId.upperID // type: 'MY_ID'
myId.camelID // type: 'myId'
See playground

typescript template literal in interface key error

Typescript v4.4.3
Reproducible Playground Example
--
interface IDocument {
[added_: `added_${string}`]: number[] | undefined;
}
const id = 'id';
const document: IDocument = {
[`added_${id}`]: [1970]
}
What i've tried:
I've confirmed that id in my code is a string.
This happens when running tsc not just in VSCode warnings
[`added_abc`]: [1] // no error
[`added_${'abc'}`]: [1] // errors
[`added_${stringVariable}`] // errors
Is there some restrictions of using template literals or anything else I can investigate to diagnose this?
'string' and '`added_${string}`' index signatures are incompatible.
Type 'string | string[] | number[]' is not assignable to type 'number[] | undefined'.
Type 'string' is not assignable to type 'number[] | undefined'.ts(2322)
The issue is that computed keys of types that are not single literal types are widened to string, and such object literals that use them will end up being given a full string index signature instead of anything narrower. So something like {[k]: 123} will be given a narrow key if k is of type "foo" ({foo: number}), but if k is of a union type type "foo" | "bar" or a pattern template literal type (as implemented in ms/TS#40598) like `foo${string}`, then it will get a full string index signature ({[x: string]: number}).
There is an open issue at microsoft/TypeScript#13948 asking for something better here; it's been around a long time and originally was asking only about unions of literals. Now that pattern template literals exist this behavior is even more noticeable. For now there is no built-in support in the language to deal with this.
In your code, tech1.uuid is of type string... not a string literal type, because the compiler infers string property types as string and not more narrowly. If you want a narrower literal type there, you might want to give tech's initializer a const assertion:
const tech1 = {
uuid: '70b26275-5096-4e4b-9d50-3c965c9e5073',
} as const;
/* const tech1: {
readonly uuid: "70b26275-5096-4e4b-9d50-3c965c9e5073";
} */
Then to get the computed key to be a single literal, you will need another const assertion to tell the compiler that is should actually process the template literal value `added_${tech1.uuid}` as a template literal type:
const doc: IDocument = {
name: "",
[`added_${tech1.uuid}` as const]: [19700101], // <-- const assert in there
}; // okay
(They almost made such things happen automatically without a const assertion, but it broke too much code and was reverted in microsoft/TypeScript#42588).
If you need tech1.uuid to remain string and want more strongly-typed computed keys, then you will need to work around it with a helper function. Here's one which takes a key of type K and a value pf type V and returns an object whose type is a type whose keys are in K and whose values are in V. (It distributes over unions, since kv(Math.random()<0.5 ? "a" : "b", 123) should have type {a: number} | {b: number} and not {a: number, b: number}:
function kv<K extends PropertyKey, V>(k: K, v: V):
{ [P in K]: { [Q in P]: V } }[K] {
return { [k]: v } as any;
}
You can see that it behaves as desired with a pattern template literal key:
const test = kv(`added_${tech1.uuid}` as const, [19700101]);
/* const test: { [x: `added_${string}`]: number[]; } */
And so you can use it along with Object.assign() to build the object you want as an IDocument:
const doc: IDocument = Object.assign(
{ name: "" },
kv(`added_${tech1.uuid}` as const, [19700101])
)
(Note that while you should be able to write {name: "", ...kv(`added_${tech1.uuid}` as const, [19700101])}, this isn't really working safely because the index signature is removed. See microsoft/TypeScript#42021 for more information.)
This may or may not be worth it to you; probably you can just write a type assertion and move on:
const doc = {
name: "",
[`added_${tech1.uuid}`]: [19700101],
} as IDocument;
This is less safe than the prior solutions but it's very easy.
Playground link to code
You need to assure TypeScript that tech1.uuid is a constant value.
interface IDocument {
name: string;
[added_: `added_${string}`]: number[] | undefined;
}
const tech1 = {
uuid: '70b26275-5096-4e4b-9d50-3c965c9e5073',
} as const;
const doc: IDocument = {
name: "",
[`added_${ tech1.uuid }` as const]: [19700101],
};
Playground

TypeScript - How do you chain accessing optional nested type properties?

I have a Client class that stores caches of other objects that the application needs to keep in memory. The structure of the object's cache is developer-defined. For example, if we have a cache of Example objects:
class Example {
property1: string;
property2: string;
}
The developer might only want property1 cached.
import { EventEmitter } from "events";
// Constructor options for `Client`
interface ClientData {
cacheStrategies?: CacheStrategies;
}
// How various objects should be cached
interface CacheStrategies {
example?: ExampleCacheStrategies;
...
}
// Metadata for how each type of object should be cached
interface ExampleCacheStrategies {
cacheFor?: number;
customCache?: ExampleCustomCacheData;
}
// The custom structure of what parts of `example` objects should be cached
interface ExampleCustomCacheData {
property1?: boolean;
property2?: boolean;
}
// The object stored in `Client.exampleCache`, based on the custom structure defined in `ExampleCustomCacheData`
interface CustomExampleData<CachedExampleProperties extends ExampleCustomCacheData> {
property1: CachedExampleProperties["property1"] extends true ? string /* 1 */ : undefined;
property2: CachedExampleProperties["property2"] extends true ? string : undefined;
}
class Client<ClientOptions extends ClientData> extends EventEmitter {
// The map's value should be based on the custom structure defined in `ExampleCustomCacheData`
exampleCache: Map<string, CustomExampleData<ClientOptions["cacheStrategies"]["example"]["customCache"]>>;
constructor(clientData: ClientOptions) {
super();
this.exampleCache = new Map();
}
}
const client = new Client({
cacheStrategies: {
example: {
/**
* The properties of `example` objects that should be cached
* This says that `property1` should be cached (string (1))
*/
customCache: {
property1: true, // (2)
... // (3)
}
}
}
});
client.exampleCache.set("123", {
property1: "value"
});
const exampleObject = client.exampleCache.get("123");
if (exampleObject) {
// Should be `string` instead of `string | undefined` (2)
console.log(exampleObject.property1);
// `string | undefined`, as expected since it's falsey (3)
console.log(exampleObject.property2);
}
As explained in the comments above the console.log()s, the goal is for objects that are pulled from the cache to have property1 be a string instead of string | undefined.
The problem is that exampleCache: Map<string, CustomExampleData<ClientOptions["cacheStrategies"]["example"]["customCache"]>>; doesn't work since both ClientOptions["cacheStrategies"] and ClientOptions["cacheStrategies"]["example"] are optional. The following doesn't work either:
exampleCache: Map<string, CustomExampleData<ClientOptions["cacheStrategies"]?.["example"]?.["customCache"]>>;
It errors with '>' expected at ?.. How can I solve this?
Syntax like the optional chaining operator ?. or the non-null assertion operator ! only applies to value expressions that will make it through to JavaScript in some form. But you need something that works with type expressions which exist only in the static type system and are erased when transpiled.
There is a NonNullable<T> utility type which is the type system analog of the non-null assertion operator. Given a union type T, the type NonNullable<T> will be the same as T but without any union members of type null or undefined:
type Foo = string | number | undefined;
type NonNullableFoo = NonNullable<Foo>;
// type NonNullableFoo = string | number
In fact, the compiler actually uses it to represent the type of an expression that has the non-null assertion operator applied to it:
function nonNullAssertion<T>(x: T) {
const nonNullX = x!;
// const nonNullX: NonNullable<T>
}
So, everywhere you have a type T that includes null or undefined and you would like to remove it, you can use NonNullable<T>. In your code, you will need to do it multiple times. In the interest of something like brevity (of code, not my explanation), let's use a shorter alias:
type NN<T> = NonNullable<T>;
and then
class Client<C extends ClientData> extends EventEmitter {
exampleCache: Map<string, CustomExampleData<
NN<NN<NN<C["cacheStrategies"]>["example"]>["customCache"]>>
>;
}
This compiles without error, and behaves how I think you'd like it:
console.log(exampleObject.property1.toUpperCase()); // string
console.log(exampleObject.property2); // undefined
Playground link to code

How would I implement this type in Typescript (from Flow?) [duplicate]

Is there a way to extend the built-in Record (or a { [key:string]: string } interface) where you also define some fixed keys and their types?
Let's say we have this:
const otherValues = {
some: 'some',
other: 'other',
values: 'values',
}
const composedDictionary = {
id: 1,
...otherValues
}
I want to define an interface for composedDictionary where id is typed as number (and number only) and everything else as string.
I've tried this:
interface ExtendedRecord extends Record<string, string> {
id: number;
}
and this:
interface MyDictionary {
[key: string]: string;
id: number;
}
Both fail with:
Property 'id' of type 'number' is not assignable to string index type 'string'
Any ideas?
Ideally the index signature should reflect any possible indexing operation result. If you access composedDictionary with an uncheckable string key the result might be number if that string is actually 'id' (eg: composedDictionary['id' as string], the typings would say this is string but at runtime it turns out to be number). This is why the type system is fighting you on this, this is an inconsistent type.
You can define your index to be consistent will all properties:
interface MyDictionary {
[key: string]: string | number;
id: number;
}
There is a loophole to the checks typescript does for index and property consistency. That loop hole is intersection types:
type MyDictionary = Record<string, string> & {
id: number
}
const composedDictionary: MyDictionary = Object.assign({
id: 1,
}, {
...otherValues
});
The compiler will still fight you on assignment, and the only way to create such an inconsistent object in the type system is by using Object.assign
As the other answer said, TypeScript does not support a type in which some properties are exceptions to the index signature... and thus there is no way to represent your MyDictionary as a consistent concrete type. The inconsistent-intersection solution ({[k: string]: string]} & {id: number}) happens to work on property reads, but is difficult to use with property writes.
There was an old suggestion to allow "rest" index signatures where you can say that an index signature is supposed to represent all properties except for those specified.
There's also a more recent (but possibly shelved) pair of enhancements implementing negated types and arbitrary key types for index signatures, which would allow you to represent such exception/default index signature properties as something like { id: number; [k: string & not "id"]: string }. But that doesn't compile yet (TS3.5) and may never compile, so this is just a dream for now.
So you can't represent MyDictionary as a concrete type. You can, however, represent it as a generic constraint. Using it suddenly requires that all your previously concrete functions must become generic functions, and your previously concrete values must become outputs of generic functions. So it might be too much machinery than it's worth. Still, let's see how to do it:
type MyDictionary<T extends object> = { id: number } & {
[K in keyof T]: K extends "id" ? number : string
};
In this case, MyDictionary<T> takes a candidate type T and transforms it into a version which matches your desired MyDictionary type. Then we use the following helper function to check if something matches:
const asMyDictionary = <T extends MyDictionary<T>>(dict: T) => dict;
Notice the self-referencing constraint T extends MyDictionary<T>. So here's your use case, and how it works:
const otherValues = {
some: "some",
other: "other",
values: "values"
};
const composedDictionary = asMyDictionary({
id: 1,
...otherValues
}); // okay
That compiles with no errors, because the parameter to asMyDictionary() is a valid MyDictionary<T>. Now let's see some failures:
const invalidDictionary = asMyDictionary({
id: 1,
oops: 2 // error! number is not a string
})
const invalidDictionary2 = asMyDictionary({
some: "some" // error! property id is missing
})
const invalidDictionary3 = asMyDictionary({
id: "oops", // error! string is not a number
some: "some"
})
The compiler catches each of those mistakes and tells you where the problem is.
So, this is the closest I can get to what you want in as of TS3.5. Okay, hope that helps; good luck!
Link to code

How to reduce javascript object to only contain properties from interface

When using typescript a declared interface could look like this:
interface MyInterface {
test: string;
}
And an implementation with extra property could be like this:
class MyTest implements MyInterface {
test: string;
newTest: string;
}
Example (here the variable 'reduced' still contain the property 'newTest'):
var test: MyTest = {test: "hello", newTest: "world"}
var reduced: MyInterface = test; // something clever is needed
Question
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Why
The problem occur when trying to use the 'reduced' variable with angular.toJson before sending it to a rest service - the toJson method transforms the newTest variable, even if it's not accessible on the instance during compile, and this makes the rest service not accept the json since it has properties that shouldn't be there.
It is not possible to do this. The reason being interface is a Typescript construct and the transpiled JS code is empty
//this code transpiles to empty!
interface MyInterface {
test: string;
}
Thus at runtime there is nothing to 'work with' - no properties exist to interrogate.
The answer by #jamesmoey explains a workaround to achieve the desired outcome.
A similar solution I use is simply to define the 'interface' as a class -
class MyInterface {
test: string = undefined;
}
Then you can use lodash to pick the properties from the 'interface' to inject into you object:
import _ from 'lodash'; //npm i lodash
const before = { test: "hello", newTest: "world"};
let reduced = new MyInterface();
_.assign(reduced , _.pick(before, _.keys(reduced)));
console.log('reduced', reduced)//contains only 'test' property
see JSFiddle
This is a pragmatic solution that has served me well without getting bogged down on semantics about whether it actually is an interface and/or naming conventions (e.g. IMyInterface or MyInterface) and allows you to mock and unit test
TS 2.1 has Object Spread and Rest, so it is possible now:
var my: MyTest = {test: "hello", newTest: "world"}
var { test, ...reduced } = my;
After that reduced will contain all properties except of "test".
Another possible approach:
As other answers have mentioned, you can't avoid doing something at runtime; TypeScript compiles to JavaScript, mostly by simply removing interface/type definitions, annotations, and assertions. The type system is erased, and your MyInterface is nowhere to be found in the runtime code that needs it.
So, you will need something like an array of keys you want to keep in your reduced object:
const myTestKeys = ["test"] as const;
By itself this is fragile, since if MyInterface is modified, your code might not notice. One possible way to make your code notice is to set up some type alias definitions that will cause a compiler error if myTestKeys doesn't match up with keyof MyInterface:
// the following line will error if myTestKeys has entries not in keyof MyInterface:
type ExtraTestKeysWarning<T extends never =
Exclude<typeof myTestKeys[number], keyof MyInterface>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_EXTRA_KEY_NAMES_HERE' does not satisfy the constraint 'never'
// the following line will error if myTestKeys is missing entries from keyof MyInterface:
type MissingTestKeysWarning<T extends never =
Exclude<keyof MyInterface, typeof myTestKeys[number]>> = void;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type 'UNION_OF_MISSING_KEY_NAMES_HERE' does not satisfy the constraint 'never'
That's not very pretty, but if you change MyInterface, one or both of the above lines will give an error that hopefully is expressive enough that the developer can modify myTestKeys.
There are ways to make this more general, or possibly less intrusive, but almost no matter what you do, the best you can reasonably expect from TypeScript is that your code will give compiler warnings in the face of unexpected changes to an interface; not that your code will actually do different things at runtime.
Once you have the keys you care about you can write a pick() function that pulls just those properties out of an object:
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
return keys.reduce((o, k) => (o[k] = obj[k], o), {} as Pick<T, K>);
}
And them we can use it on your test object to get reduced:
var test: MyTest = { test: "hello", newTest: "world" }
const reduced: MyInterface = pick(test, ...myTestKeys);
console.log(JSON.stringify(reduced)); // {"test": "hello"}
That works!
Playground link to code
Are you trying to only set/assign properties listed on the interface only? Functionality like that is not available in TypeScript but it is very simple to write a function to perform the behaviour you looking for.
interface IPerson {
name: string;
}
class Person implements IPerson {
name: string = '';
}
class Staff implements IPerson {
name: string = '';
position: string = '';
}
var jimStaff: Staff = {
name: 'Jim',
position: 'Programmer'
};
var jim: Person = new Person();
limitedAssign(jimStaff, jim);
console.log(jim);
function limitedAssign<T,S>(source: T, destination: S): void {
for (var prop in destination) {
if (source[prop] && destination.hasOwnProperty(prop)) {
destination[prop] = source[prop];
}
}
}
In your example newTest property won't be accessible thru the reduced variable, so that's the goal of using types. The typescript brings type checking, but it doesn't manipulates the object properties.
In a general way, how can you make the 'reduced' variable to only contain the properties declared in the 'MyInterface' interface.
Since TypeScript is structural this means that anything that contains the relevant information is Type Compatible and therefore assignable.
That said, TypeScript 1.6 will get a concept called freshness. This will make it easier to catch clear typos (note freshness only applies to object literals):
// ERROR : `newText` does not exist on `MyInterface`
var reduced: MyInterface = {test: "hello", newTest: "world"};
Easy example:
let all_animals = { cat: 'bob', dog: 'puka', fish: 'blup' };
const { cat, ...another_animals } = all_animals;
console.log(cat); // bob
One solution could be to use a class instead of an interface and use a factory method (a public static member function that returns a new object of it's type). The model is the only place where you know the allowed properties and it's the place where you don't forget to update them accidentaly on model changes.
class MyClass {
test: string;
public static from(myClass: MyClass): MyClass {
return {test: myClass.test};
}
}
Example:
class MyTest extends MyClass {
test: string;
newTest: string;
}
const myTest: MyTest = {test: 'foo', newTest: 'bar'};
const myClass: MyClass = MyClass.from(myTest);

Categories