Interface accepting any number of key value objects [duplicate] - javascript

Existing JavaScript code has "records" where the id is numeric and the other attributes string.
Trying to define this type:
type T = {
id: number;
[key:string]: string
}
gives error 2411 id type number not assignable to string

There is no specific type in TypeScript that corresponds to your desired structure. String index signatures must apply to every property, even the manually declared ones like id. What you're looking for is something like a "rest index signature" or a "default property type", and there is an open suggestion in GitHub asking for this: microsoft/TypeScript#17867. A while ago there was some work done that would have enabled this, but it was shelved (see this comment for more info). So it's not clear when or if this will happen.
You could widen the type of the index signature property so it includes the hardcoded properties via a union, like
type WidenedT = {
id: number;
[key: string]: string | number
}
but then you'd have to test every dynamic property before you could treat it as a string:
function processWidenedT(t: WidenedT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error
if (typeof t.random === "string") t.random.toUpperCase(); // okay
}
The best way to proceed here would be if you could refactor your JavaScript so that it doesn't "mix" the string-valued bag of properties with a number-valued id. For example:
type RefactoredT = {
id: number;
props: { [k: string]: string };
}
Here id and props are completely separate and you don't have to do any complicated type logic to figure out whether your properties are number or string valued. But this would require a bunch of changes to your existing JavaScript and might not be feasible.
From here on out I'll assume you can't refactor your JavaScript. But notice how clean the above is compared to the messy stuff that's coming up:
One common workaround to the lack of rest index signatures is to use an intersection type to get around the constraint that index signatures must apply to every property:
type IntersectionT = {
id: number;
} & { [k: string]: string };
It sort of kind of works; when given a value of type IntersectionT, the compiler sees the id property as a number and any other property as a string:
function processT(t: IntersectionT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // okay
t.id = 1; // okay
t.random = "hello"; // okay
}
But it's not really type safe, since you are technically claiming that id is both a number (according to the first intersection member) and a string (according to the second intersection member). And so you unfortunately can't assign an object literal to that type without the compiler complaining:
t = { id: 1, random: "hello" }; // error!
// Property 'id' is incompatible with index signature.
You have to work around that further by doing something like Object.assign():
const propBag: { [k: string]: string } = { random: "" };
t = Object.assign({ id: 1 }, propBag);
But this is annoying, since most users will never think to synthesize an object in such a roundabout way.
A different approach is to use a generic type to represent your type instead of a specific type. Think of writing a type checker that takes as input a candidate type, and returns something compatible if and only if that candidate type matches your desired structure:
type VerifyT<T> = { id: number } & { [K in keyof T]: K extends "id" ? unknown : string };
This will require a generic helper function so you can infer the generic T type, like this:
const asT = <T extends VerifyT<T>>(t: T) => t;
Now the compiler will allow you to use object literals and it will check them the way you expect:
asT({ id: 1, random: "hello" }); // okay
asT({ id: "hello" }); // error! string is not number
asT({ id: 1, random: 2 }); // error! number is not string
asT({ id: 1, random: "", thing: "", thang: "" }); // okay
It's a little harder to read a value of this type with unknown keys, though. The id property is fine, but other properties will not be known to exist, and you'll get an error:
function processT2<T extends VerifyT<T>>(t: T) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error! random not known to be a property
}
Finally, you can use a hybrid approach that combines the best aspects of the intersection and generic types. Use the generic type to create values, and the intersection type to read them:
function processT3<T extends VerifyT<T>>(t: T): void;
function processT3(t: IntersectionT): void {
t.id.toFixed();
if ("random" in t)
t.random.toUpperCase(); // okay
}
processT3({ id: 1, random: "hello" });
The above is an overloaded function, where callers see the generic type, but the implementation sees the intersection type.
Playground link to code

You are getting this error since you have declared it as Indexable Type (ref: https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types) with string being the key type, so id being a number fails to conform to that declaration.
It is difficult to guess your intention here, but may be you wanted something like this:
class t {
id: number;
values = new Map<string, string>();
}

I had this same issue, but returned the id as a string.
export type Party = { [key: string]: string }
I preferred to have a flat type and parseInt(id) on the receiving code.
For my API, the simplest thing that could possibly work.

Related

Can i create a type that includes itself recursively? [duplicate]

Existing JavaScript code has "records" where the id is numeric and the other attributes string.
Trying to define this type:
type T = {
id: number;
[key:string]: string
}
gives error 2411 id type number not assignable to string
There is no specific type in TypeScript that corresponds to your desired structure. String index signatures must apply to every property, even the manually declared ones like id. What you're looking for is something like a "rest index signature" or a "default property type", and there is an open suggestion in GitHub asking for this: microsoft/TypeScript#17867. A while ago there was some work done that would have enabled this, but it was shelved (see this comment for more info). So it's not clear when or if this will happen.
You could widen the type of the index signature property so it includes the hardcoded properties via a union, like
type WidenedT = {
id: number;
[key: string]: string | number
}
but then you'd have to test every dynamic property before you could treat it as a string:
function processWidenedT(t: WidenedT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error
if (typeof t.random === "string") t.random.toUpperCase(); // okay
}
The best way to proceed here would be if you could refactor your JavaScript so that it doesn't "mix" the string-valued bag of properties with a number-valued id. For example:
type RefactoredT = {
id: number;
props: { [k: string]: string };
}
Here id and props are completely separate and you don't have to do any complicated type logic to figure out whether your properties are number or string valued. But this would require a bunch of changes to your existing JavaScript and might not be feasible.
From here on out I'll assume you can't refactor your JavaScript. But notice how clean the above is compared to the messy stuff that's coming up:
One common workaround to the lack of rest index signatures is to use an intersection type to get around the constraint that index signatures must apply to every property:
type IntersectionT = {
id: number;
} & { [k: string]: string };
It sort of kind of works; when given a value of type IntersectionT, the compiler sees the id property as a number and any other property as a string:
function processT(t: IntersectionT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // okay
t.id = 1; // okay
t.random = "hello"; // okay
}
But it's not really type safe, since you are technically claiming that id is both a number (according to the first intersection member) and a string (according to the second intersection member). And so you unfortunately can't assign an object literal to that type without the compiler complaining:
t = { id: 1, random: "hello" }; // error!
// Property 'id' is incompatible with index signature.
You have to work around that further by doing something like Object.assign():
const propBag: { [k: string]: string } = { random: "" };
t = Object.assign({ id: 1 }, propBag);
But this is annoying, since most users will never think to synthesize an object in such a roundabout way.
A different approach is to use a generic type to represent your type instead of a specific type. Think of writing a type checker that takes as input a candidate type, and returns something compatible if and only if that candidate type matches your desired structure:
type VerifyT<T> = { id: number } & { [K in keyof T]: K extends "id" ? unknown : string };
This will require a generic helper function so you can infer the generic T type, like this:
const asT = <T extends VerifyT<T>>(t: T) => t;
Now the compiler will allow you to use object literals and it will check them the way you expect:
asT({ id: 1, random: "hello" }); // okay
asT({ id: "hello" }); // error! string is not number
asT({ id: 1, random: 2 }); // error! number is not string
asT({ id: 1, random: "", thing: "", thang: "" }); // okay
It's a little harder to read a value of this type with unknown keys, though. The id property is fine, but other properties will not be known to exist, and you'll get an error:
function processT2<T extends VerifyT<T>>(t: T) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error! random not known to be a property
}
Finally, you can use a hybrid approach that combines the best aspects of the intersection and generic types. Use the generic type to create values, and the intersection type to read them:
function processT3<T extends VerifyT<T>>(t: T): void;
function processT3(t: IntersectionT): void {
t.id.toFixed();
if ("random" in t)
t.random.toUpperCase(); // okay
}
processT3({ id: 1, random: "hello" });
The above is an overloaded function, where callers see the generic type, but the implementation sees the intersection type.
Playground link to code
You are getting this error since you have declared it as Indexable Type (ref: https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types) with string being the key type, so id being a number fails to conform to that declaration.
It is difficult to guess your intention here, but may be you wanted something like this:
class t {
id: number;
values = new Map<string, string>();
}
I had this same issue, but returned the id as a string.
export type Party = { [key: string]: string }
I preferred to have a flat type and parseInt(id) on the receiving code.
For my API, the simplest thing that could possibly work.

In TypeScript, how can I use enum as keys in a type but with additional fields? [duplicate]

Existing JavaScript code has "records" where the id is numeric and the other attributes string.
Trying to define this type:
type T = {
id: number;
[key:string]: string
}
gives error 2411 id type number not assignable to string
There is no specific type in TypeScript that corresponds to your desired structure. String index signatures must apply to every property, even the manually declared ones like id. What you're looking for is something like a "rest index signature" or a "default property type", and there is an open suggestion in GitHub asking for this: microsoft/TypeScript#17867. A while ago there was some work done that would have enabled this, but it was shelved (see this comment for more info). So it's not clear when or if this will happen.
You could widen the type of the index signature property so it includes the hardcoded properties via a union, like
type WidenedT = {
id: number;
[key: string]: string | number
}
but then you'd have to test every dynamic property before you could treat it as a string:
function processWidenedT(t: WidenedT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error
if (typeof t.random === "string") t.random.toUpperCase(); // okay
}
The best way to proceed here would be if you could refactor your JavaScript so that it doesn't "mix" the string-valued bag of properties with a number-valued id. For example:
type RefactoredT = {
id: number;
props: { [k: string]: string };
}
Here id and props are completely separate and you don't have to do any complicated type logic to figure out whether your properties are number or string valued. But this would require a bunch of changes to your existing JavaScript and might not be feasible.
From here on out I'll assume you can't refactor your JavaScript. But notice how clean the above is compared to the messy stuff that's coming up:
One common workaround to the lack of rest index signatures is to use an intersection type to get around the constraint that index signatures must apply to every property:
type IntersectionT = {
id: number;
} & { [k: string]: string };
It sort of kind of works; when given a value of type IntersectionT, the compiler sees the id property as a number and any other property as a string:
function processT(t: IntersectionT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // okay
t.id = 1; // okay
t.random = "hello"; // okay
}
But it's not really type safe, since you are technically claiming that id is both a number (according to the first intersection member) and a string (according to the second intersection member). And so you unfortunately can't assign an object literal to that type without the compiler complaining:
t = { id: 1, random: "hello" }; // error!
// Property 'id' is incompatible with index signature.
You have to work around that further by doing something like Object.assign():
const propBag: { [k: string]: string } = { random: "" };
t = Object.assign({ id: 1 }, propBag);
But this is annoying, since most users will never think to synthesize an object in such a roundabout way.
A different approach is to use a generic type to represent your type instead of a specific type. Think of writing a type checker that takes as input a candidate type, and returns something compatible if and only if that candidate type matches your desired structure:
type VerifyT<T> = { id: number } & { [K in keyof T]: K extends "id" ? unknown : string };
This will require a generic helper function so you can infer the generic T type, like this:
const asT = <T extends VerifyT<T>>(t: T) => t;
Now the compiler will allow you to use object literals and it will check them the way you expect:
asT({ id: 1, random: "hello" }); // okay
asT({ id: "hello" }); // error! string is not number
asT({ id: 1, random: 2 }); // error! number is not string
asT({ id: 1, random: "", thing: "", thang: "" }); // okay
It's a little harder to read a value of this type with unknown keys, though. The id property is fine, but other properties will not be known to exist, and you'll get an error:
function processT2<T extends VerifyT<T>>(t: T) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error! random not known to be a property
}
Finally, you can use a hybrid approach that combines the best aspects of the intersection and generic types. Use the generic type to create values, and the intersection type to read them:
function processT3<T extends VerifyT<T>>(t: T): void;
function processT3(t: IntersectionT): void {
t.id.toFixed();
if ("random" in t)
t.random.toUpperCase(); // okay
}
processT3({ id: 1, random: "hello" });
The above is an overloaded function, where callers see the generic type, but the implementation sees the intersection type.
Playground link to code
You are getting this error since you have declared it as Indexable Type (ref: https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types) with string being the key type, so id being a number fails to conform to that declaration.
It is difficult to guess your intention here, but may be you wanted something like this:
class t {
id: number;
values = new Map<string, string>();
}
I had this same issue, but returned the id as a string.
export type Party = { [key: string]: string }
I preferred to have a flat type and parseInt(id) on the receiving code.
For my API, the simplest thing that could possibly work.

Constraining typescript interface keys and values

I am trying to use an interface that is declared few times with different key-value pairs and I want to restrict its keys to a template literal and values to a given generic type. This interface will be later used to define function input. I tried to check if key extends string, and it seemed to work but I was able to pass anything as a key, even number.
Here is the snippet of what I'd like to achieve:
// restrict interface keys and values
interface Test{
[k: string | number | symbol]: typeof k extends `${infer Prefix}#${infer Suffix}` ? { prefix: Prefix, suffix: Suffix } : never
}
interface Test{
'test1#test': { prefix: 'test1', suffix: 'test' }, // this throws, but shouldnt
}
interface Test{
'test2#test': { prefix: 'test2', suffix: 'test' }, // this throws, but shouldnt
}
There's unfortunately no way to constrain an interface the way you're trying to do here, at least not so that errors will appear directly on the lines you don't like. All my attempts ran afoul of circularity detectors, like
type CheckTest = { [K in keyof Test]:
K extends `${infer P}#${infer S}` ? { prefix: P, suffix: S } : never
};
interface Test extends CheckTest { } // error, circular!
Even if that didn't error, there's no implements clause for interfaces, so the constraint would be hard to represent directly. There's an open issue at microsoft/TypeScript#24274, and the workarounds listed in there are illegally circular, at least when I apply them here.
The closest I can think of is a line that checks whether Test is valid according to your rules, and which results in a compiler error at or near that line if there's a problem. This error will hopefully contain enough information for someone to find and fix the offending member of Test, although this is made less pleasant when you are using declaration merging and your members of Test are scattered throughout your code base.
Oh well, let's at least look at it:
type CheckTest = {
[K in keyof Test]: K extends `${infer P}#${infer S}` ?
{ prefix: P, suffix: S } : never
};
type VerifyTest<T extends Test = CheckTest> = T;
// ----------------------------> ^^^^^^^^^ <----- IS THERE AN ERROR HERE?
// if so, then some member of Test is bad.
// The error will tell you which one and what's wrong
The CheckTest type is similar to your definition except that it actually maps over the properties of Test instead of trying to use an index signature which treats the key as a single thing (typeof k is just string | number | symbol and not the specific keys of Test).
Anyway, CheckTest should be assignable to Test if Test is valid. Then the VerifyTest definition just assumes it's valid, by using CheckTest in a type position that needs to be assignable to Test.
Let's test it out. First we have this:
interface Test {
'test1#test': { prefix: 'test1', suffix: 'test' }
}
interface Test {
'test2#test': { prefix: 'test2', suffix: 'test' }
}
// ...
type VerifyTest<T extends Test = CheckTest> = T; // okay
No error, so we're good. Then someone comes along and does a bad thing:
interface Test {
'foo#bar': { prefix: "oof", suffix: "bar" }
}
And suddenly
type VerifyTest<T extends Test = CheckTest> = T; // error!
// ----------------------------> ~~~~~~~~~
/* The types of ''foo#bar'.prefix' are incompatible between these types.
Type '"foo"' is not assignable to type '"oof"' */
Which lets you know that it's time to find "foo"/"oof" in your code base and fix it.
Not pretty, but it's the closest I can get.
Playground link to code

TypeScript conditional type and computed object property names

I'm having trouble using a conditional type in combination with a computed object property name. Basically I'm inserting rows into a database based on an input string. Then I'm typing the return object based on that input string. One of the properties in the return object is a computed name that is also based on the input string. So it seems like typescript would have all the info needed to verify that this is correct but it keeps giving me errors. Here's a very simplified example.
//types of the table rows from the database
interface FirstTableRow {
id: number,
someFirstRefId: number
};
interface SecondTableRow {
id: number,
someSecondRefId: number
};
//maps which table we're working with to its reference column name
const whichToExtraColumn = {
first: 'someFirstRefId',
second: 'someSecondRefId'
} as const;
//maps the table to the returned row type
type ConstToObj<T> = (T extends 'first'
? FirstTableRow
: T extends 'second'
? SecondTableRow
: never
);
function createFirstOrSecond<
T extends keyof typeof whichToExtraColumn
>(
which: T
): ConstToObj<T> {
//gets the reference column name for this table
const refColumn = whichToExtraColumn[which];
//do database stuff....
const insertId = 1;
//build the inserted row
const test: ConstToObj<T> = {
id: insertId,
[refColumn]: 123
};
// ^ Type '{ [x: string]: number; id: number; }' is not assignable to type 'ConstToObj<T>'
return test;
};
I made a workaround by doing an if-check on refColumn, then generating different objects depending on that. But using a computed property name would be waayyy easier. Any help would be appreciated.
You are running into multiple issues here:
(1) Computed property names are widened, one might say this is a bug:
type Key = "a" | "b";
let a: Key = Math.random() ? "a" : "b";
const result = { [a]: 1 };
// -> { [x: string]: number }
So your example, [refColumn]: 123 will never behave as you want it to.
(2) Function bodies of functions with generic parameters are not validated iteratively with all possible subtypes (I guess the compiler might run forever then), instead they are validated with the type constraint. Thus if you have two generic types, whereas one is derived from the other, Typescript simply does not care. Usually this is not a problem because usually one type is directly the subtype of the other:
function assign<A extends B, B extends 1 | 2 | 3>(a: A) {
const b: B = a;
}
You've created a case where this is not the case, and constraint checking will always fail.
(3) One just cannot assign to a deferred conditional type. Typescript does not know which branch the conditional type will take (if it's evaluation is deferred), and as such only any can be assigned to it.
function plusOne<A extends 1 | 2>(a: A) {
const b: (A extends 1 ? 2 : 3) = a + 1;
}
So with these three limitations it is basically impossible to write your function without manual typecasts. This is one of the few cases were an as any seems very reasonable.

Typescript - how can a exclude a type from a generic type without using Exclude<>?

I have a function that stores different values into the local storage by stringifying them and I want to limit the function from being able to work with Moment objects. The syntax would be this:
public static set<TValue>(
key: LocalStorageKeyEnum,
value: TValue,
...keySuffixes: string[]
) {
localStorage.setItem(
LocalStorage.buildKey(key, keySuffixes),
JSON.stringify(value)
)
}
The thing is the function would work without any issues if the second argument would be a Moment object and I want to exclude this type when writing <TValue>. Is there a way to do this using Typescript or the only way to go is running a check on run-time? I could work with 2 types, where the second would be a type that excludes the property of type Moment, like so
type Variants = {
value: TValue,
date: moment.Moment
}
type ExcludeDate = Exclude {typeof Variants, "date"}
but I was wondering if there's another way to do it. Thank you! I am new to Typescript so I am sorry if I wasn't very clear.
You can exclude type by conditional type:
type MomentType = { x: string } // just an example simulation of moment
function set<TValue>(
key: string,
value: TValue extends MomentType ? never : TValue, // pay attention here
...keySuffixes: string[]
) {
// implementation
}
set('key', { x: 'a' }) // error as its the MomentType
set('key', { y: 'a' }) // ok as its not the MomentType
The key line is value: TValue extends MomentType ? never : TValue. We say that if passed type extends our MomentType then the value is of the type never, what means you cannot pass value to it, as never is empty type (there is no instance of never).
MomentType was used only for example purposes, it can be any other type you want to exclude.

Categories