How to change value of JSONValue?
My code:
type JSONValue =
| string
| number
| boolean
| { [x: string]: JSONValue }
| Array<JSONValue>;
interface Data {
name: string;
values: JSONValue;
}
const d: Data = {
name: "John",
values: {"first": 1, "second": 2}
}
I would like to change the "first" value:
d.values["first"] = 2;
but this return me error:
Element implicitly has an 'any' type because expression of type '"first"' can't be used to index type 'JSONValue'.
Property 'first' does not exist on type 'JSONValue'.
TypeScript is complaining about the assignment because it doesn't know that you stored an object in d.values. According to your type declaration, it might as well be an array, string, number or boolean, and assigning .first to those is not allowed.
If you want to tell TypeScript "but I know it's (currently) an object", you can use a type assertion:
(d.values as { [x: string]: JSONValue })["first"] = 2;
If you want to tell TypeScript that data.values is always an object that may hold arbitrary properties, change the definition of your Data interface:
interface Data {
name: string;
values: { [x: string]: JSONValue };
}
If you are not actually certain yourself, check the type first before assigning:
if (typeof d.values == 'object' && !Array.isArray(d.values)) {
d.values["first"] = 2;
}
Related
I have a disciminated union
type MyDUnion = { type: "anon"; name: string } | { type: "google"; idToken: string };
I want to access the name key from the discriminative union, from the MyDUnion type directly. Something like this
type Name = MyDUnion['name']
But typescript won't allow that
Property 'name' doesn't exist on type '{ type: "anon"; name: string } | { type: "google"; idToken: string }'
How can I access it?
To be clear, this is not a valid solution:
type MyName = string;
type MyDUnion = { type: "anon"; name: MyName } | { type: "google"; idToken: string };
type Name = MyName; // this line would be in a different file
This is not valid, because then I would have to export both MyName and MyDUnion types to be used elsewhere.
Any ideas?
In order to filter union of objects, usually you need to use Extract:
The easy way:
type Result = Extract<MyDUnion , {type: "anon"}>
The more robust:
type MyDUnion = { type: "anon"; name: string } | { type: "google"; idToken: string };
type Filter<Union, Type extends Partial<Union>> = Extract<Union, Type>
type Result = Filter<MyDUnion, { type: 'anon' }>
Generic Solution
export type PickAll<
Union,
Keys extends KeyOf<Union> = KeyOf<Union>,
Otherwise = undefined
> = {
[_K in Keys]: Union extends { [K in _K]?: infer Value }
? UnionToIntersection<Value>
: Otherwise
}
helpers
type KeyOf<Union, Otherwise = never> = Union extends Union
? keyof Union
: Otherwise
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
Target
type MyDUnion =
| { type: 'anon'; name: string }
| { type: 'google'; idToken: string }
indexed access and keyof
MyDUnion['type']
/* "anon' | "google" */
// OK
MyDUnion[keyof MyDUnion]
/* "anon" | "google" */
// ❓
// #ts-expect-error - not all union members have an idToken
MyDUnion['type' | 'idToken']
/* any */
// ❌
KeyOf<Union>
type UnionKeys = KeyOf<MyDUnion>
/* "type" | "name" | "idToken" */
// ✅
PickAll<Union, KeyOf<Union>?, Otherwise?>
by default, picks all
type DUnionProps = PickAll<MyDUnion>
/* {
type: "anon" | "google";
name: string | undefined;
idToken: string | undefined;
} */
// ✅
focus on a specific Key (+IDE prompts and type checking)
ctrl+space is OP
type DUnionName = PickAll<MyDUnion, 'name'>
/* {
name: string | undefined
} */
// ✅
or a union of Keys
type DesiredProps = PickAll<
MyDUnion | { fish: number },
'type' | 'idToken' | 'fish'
>
/* {
type: "anon" | "google" | undefined;
idToken: string | undefined;
fish: number | undefined;
} // ✅ */
Gotchas
Does not distinguish between undefined and optional properties. It can be done though and its on the todo.
Extract literals directly.
DON'T do this:
type should be "anon" | "google"
type GoogleLike = PickAll<MyDUnion, 'type' | 'name'>
type g = GoogleLike['name' | 'type']
/* string | undefined 🥴 */
Do this:
type GoogleLikeTypes = GoogleLike['type']
/* "anon" | "google" */
// ✅
type GoogleLikeNames = GoogleLike['name']
/* string | undefined */
// ✅ (weird, be mindful)
EDIT
I forgot to mention the third parameter can be used to change the fallback type. The default is undefined, I think this is the most type-safe behavior, but you can also set it to whatever you want. For example, PickAll<Union, Keys, never> is equivalent to Required<PickAll<Union, Keys>>, or at least it would be if Required could infer the types like PickAll.
In order to get the required key, you have to somehow tell the Typescript compiler
Hey compiler, I want to know about this object, under a very specific case
In this case, you want to know about the object, when the type ==='anon'. So, taking your example,
type MyDUnion = { type: "anon"; name: string } | { type: "google"; idToken: string };
type SpclCase = MyDUnion & {type: "anon"}
By doing so, you're getting information about when theses two cases overlap. Now you can just index the name key directly.
type Name = SpclCase['name']
If you want to make this more robust, we can make sure that the narrowing type we use ({type: 'anon'} in this case), satisfies the required shape. Here's a little impromptu solution for that.
type DiscriminatorObj = { type: 'anon' }
type RequiredCase = DiscriminatorObj extends Pick<MyDUnion, "type"> ? DiscriminatorObj: never;
type SpclCase = (RequestBody & RequiredCase);
type Name = SpclCase['name']
I know its a little rough around the edges, but you can always extract this into generic functions and use them as you please. I merely showed you the fundamental logic. You can use it to make it more aesthetically pleasing.
Here is my code
async getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
class User {
id: number;
name: string;
}
class GetAllUserData{
id: number;
}
getAll function returns User[], and each element of array has the name property, even if its return type is GetAllUserData[].
I want to know if it is possible "out of the box" in TypeScript to restrict an object only to properties specified by its type.
I figured out a way, using built-in types available since TypeScript version 3, to ensure that an object passed to a function does not contain any properties beyond those in a specified (object) type.
// First, define a type that, when passed a union of keys, creates an object which
// cannot have those properties. I couldn't find a way to use this type directly,
// but it can be used with the below type.
type Impossible<K extends keyof any> = {
[P in K]: never;
};
// The secret sauce! Provide it the type that contains only the properties you want,
// and then a type that extends that type, based on what the caller provided
// using generics.
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
// Now let's try it out!
// A simple type to work with
interface Animal {
name: string;
noise: string;
}
// This works, but I agree the type is pretty gross. But it might make it easier
// to see how this works.
//
// Whatever is passed to the function has to at least satisfy the Animal contract
// (the <T extends Animal> part), but then we intersect whatever type that is
// with an Impossible type which has only the keys on it that don't exist on Animal.
// The result is that the keys that don't exist on Animal have a type of `never`,
// so if they exist, they get flagged as an error!
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
// This is the best I could reduce it to, using the NoExtraProperties<> type above.
// Functions which use this technique will need to all follow this formula.
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
// It works for variables defined as the type
const okay: NoExtraProperties<Animal> = {
name: 'Dog',
noise: 'bark',
};
const wrong1: NoExtraProperties<Animal> = {
name: 'Cat',
noise: 'meow'
betterThanDogs: false, // look, an error!
};
// What happens if we try to bypass the "Excess Properties Check" done on object literals
// by assigning it to a variable with no explicit type?
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};
thisWorks(okay);
thisWorks(wrong1); // doesn't flag it as an error here, but does flag it above
thisWorks(wrong2); // yay, an error!
thisIsAsGoodAsICanGetIt(okay);
thisIsAsGoodAsICanGetIt(wrong1); // no error, but error above, so okay
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
Typescript can't restrict extra properties
Unfortunately this isn't currently possible in Typescript, and somewhat contradicts the shape nature of TS type checking.
Answers in this thread that relay on the generic NoExtraProperties are very elegant, but unfortunately they are unreliable, and can result in difficult to detect bugs.
I'll demonstrate with GregL's answer.
// From GregL's answer
type Impossible<K extends keyof any> = {
[P in K]: never;
};
type NoExtraProperties<T, U extends T = T> = U & Impossible<Exclude<keyof U, keyof T>>;
interface Animal {
name: string;
noise: string;
}
function thisWorks<T extends Animal>(animal: T & Impossible<Exclude<keyof T, keyof Animal>>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
function thisIsAsGoodAsICanGetIt<T extends Animal>(animal: NoExtraProperties<Animal, T>): void {
console.log(`The noise that ${animal.name.toLowerCase()}s make is ${animal.noise}.`);
}
const wrong2 = {
name: 'Rat',
noise: 'squeak',
idealScenarios: ['labs', 'storehouses'],
invalid: true,
};
thisWorks(wrong2); // yay, an error!
thisIsAsGoodAsICanGetIt(wrong2); // yay, an error!
This works if at the time of passing an object to thisWorks/thisIsAsGoodAsICanGet TS recognizes that the object has extra properties. But in TS if it's not an object literal, a value can always have extra properties:
const fun = (animal:Animal) =>{
thisWorks(animal) // No Error
thisIsAsGoodAsICanGetIt(animal) // No Error
}
fun(wrong2) // No Error
So, inside thisWorks/thisIsAsGoodAsICanGetIt you can't trust that the animal param doesn't have extra properties.
Solution
Simply use pick (Lodash, Ramda, Underscore).
interface Narrow {
a: "alpha"
}
interface Wide extends Narrow{
b: "beta"
}
const fun = (obj: Narrow) => {
const narrowKeys = ["a"]
const narrow = pick(obj, narrowKeys)
// Even if obj has extra properties, we know for sure that narrow doesn't
...
}
Typescript uses structural typing instead of nominal typing to determine type equality. This means that a type definition is really just the "shape" of a object of that type. It also means that any types which shares a subset of another type's "shape" is implicitly a subclass of that type.
In your example, because a User has all of the properties of GetAllUserData, User is implicitly a subtype of GetAllUserData.
To solve this problem, you can add a dummy property specifically to make your two classes different from one another. This type of property is called a discriminator. (Search for discriminated union here).
Your code might look like this. The name of the discriminator property is not important. Doing this will produce a type check error like you want.
async function getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
class User {
discriminator: 'User';
id: number;
name: string;
}
class GetAllUserData {
discriminator: 'GetAllUserData';
id: number;
}
I don't think it's possible with the code structure you have. Typescript does have excess property checks, which sounds like what you're after, but they only work for object literals. From those docs:
Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments.
But returned variables will not undergo that check. So while
function returnUserData(): GetAllUserData {
return {id: 1, name: "John Doe"};
}
Will produce an error "Object literal may only specify known properties", the code:
function returnUserData(): GetAllUserData {
const user = {id: 1, name: "John Doe"};
return user;
}
Will not produce any errors, since it returns a variable and not the object literal itself.
So for your case, since getAll isn't returning a literal, typescript won't do the excess property check.
Final Note: There is an issue for "Exact Types" which if ever implemented would allow for the kind of check you want here.
Following up on GregL's answer, I'd like to add support for arrays and make sure that if you've got one, all the objects in the array have no extra props:
type Impossible<K extends keyof any> = {
[P in K]: never;
};
export type NoExtraProperties<T, U extends T = T> = U extends Array<infer V>
? NoExtraProperties<V>[]
: U & Impossible<Exclude<keyof U, keyof T>>;
Note: The type recursion is only possible if you've got TS 3.7 (included) or above.
The accepted answer, with a discriminator, is right. TypeScript uses structural typing instead of nominal typing. It means that the transpiler will check to see if the structure match. Since both classes (could be interface or type) has id of type number it matches, hence interchangeable (this is true one side since User is having more properties.
While this might be good enough, the issue is that at runtime the returned data from your method getAll will contains the name property. Returning more might not be an issue, but could be if you are sending back the information somewhere else.
If you want to restrict the data to only what is defined in the class (interface or type), you have to build or spread a new object manually. Here is how it can look for your example:
function dbQuery(): User[] {
return [];
}
function getAll(): GetAllUserData[] {
const users: User[] = dbQuery();
const usersIDs: GetAllUserData[] = users.map(({id}) => ({id}));
return usersIDs;
}
class User {
id: number;
name: string;
}
class GetAllUserData {
id: number;
}
Without going with the runtime approach of pruning the fields, you could indicate to TypeScript that both classes are different with a private field. The code below won't let you return a User when the return type is set to GetAllUserData
class User {
id: number;
name: string;
}
class GetAllUserData {
private _unique: void;
id: number;
}
function getAll(): GetAllUserData[] {
return dbQuery(); // Doesn't compile here!
}
I found this another workaround:
function exactMatch<A extends C, B extends A, C = B>() { }
const a = { a: "", b: "", c: "" }
const b = { a: "", b: "", c: "", e: "" }
exactMatch<typeof a, typeof b>() //invalid
const c = { e: "", }
exactMatch<typeof a, typeof c>() //invalid
const d = { a: "", b: "", c: "" }
exactMatch<typeof a, typeof d>() //valid
const e = {...a,...c}
exactMatch<typeof b, typeof e>() //valid
const f = {...a,...d}
exactMatch<typeof b, typeof f>() //invalid
See the original Post
Link to Playground
As an option, you can go with a hack:
const dbQuery = () => [ { name: '', id: 1}];
async function getAll(): Promise<GetAllUserData[]> {
return await dbQuery(); // dbQuery returns User[]
}
type Exact<T> = {[k: string | number | symbol]: never} & T
type User = {
id: number;
name: string;
}
type GetAllUserData = Exact<{
id: number;
}>
Error this produces:
Type '{ name: string; id: number; }[]' is not assignable to type '({ [k: string]: never; [k: number]: never; [k: symbol]: never; } & { id: number; })[]'.
Type '{ name: string; id: number; }' is not assignable to type '{ [k: string]: never; [k: number]: never; [k: symbol]: never; } & { id: number; }'.
Type '{ name: string; id: number; }' is not assignable to type '{ [k: string]: never; [k: number]: never; [k: symbol]: never; }'.
Property 'name' is incompatible with index signature.
Type 'string' is not assignable to type 'never'.
When using types instead of interfaces, the property are restricted. At least in the IDE (no runtime check).
Example
type Point = {
x: number;
y: number;
}
const somePoint: Point = {
x: 10,
y: 22,
z: 32
}
It throws :
Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'. Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
I think types are good for defining closed data structures, compared to interfaces. Having the IDE yelling (actually the compiler) when the data does not match exactly the shape is already a great type guardian when developping
I want to type the following nested object. the type works when you assign it to an object, but is there a way to make it work while accessing its properties?
type NestedObject = {
[key: string]: NestedObject | string;
}
const obj: NestedObject = {
a: '123',
b: {
b_a: {
b_a_a: '456'
},
b_b: '789'
}
};
function ABC(myNestedObject: NestedObject) {
const nestedObject: NestedObject = {
// Property 'fail' does not exist on type 'string | NestedObject'.
'failProperty': myNestedObject.properties.fail
};
}
I tried to make it conditional by checking whether it extends from string,
but I'm having challenges accessing the current value for the specified key. This for instance, doesn't work:
type NestedObject = {
[K in NestedObject]: NestedObject[K] extends string ? string : NestedObject;
}
You need to narrow down whether the .properties property is a string or a NestedObject. If it's a string, then referencing myNestedObject.properties.fail would result in problems - which is why TypeScript is warning you about it.
Construct the code paths so that if the typeof results in string, you don't reference myNestedObject.properties.fail.
function ABC(myNestedObject: NestedObject) {
if (typeof myNestedObject.properties === 'string') {
throw new Error();
}
const nestedObject: NestedObject = {
// Property 'fail' does not exist on type 'string | NestedObject'.
'failProperty': myNestedObject.properties.fail
};
}
I have the following method which serves to extend an existing key-value map — e.g. a result of a db query — with other object(s) of the same type.
export function extendWith<
T extends { id: string | number },
O =
| (T["id"] extends string | number ? Record<T["id"], T> : never)
| (T["id"] extends string | number ? Partial<Record<T["id"], T>> : never)
>(obj: O, vals: T | T[]): O {
const extended = { ...obj };
const values = Array.isArray(vals) ? vals : [vals];
for (const val of values) {
if (val !== undefined) {
const prop = val["id"];
if (
typeof prop === "string" ||
typeof prop === "number" ||
typeof prop === "symbol"
) {
(extended as any)[prop] =
prop in obj ? { ...(obj as any)[prop], ...val } : val;
}
}
}
return extended;
}
When I call it as follows, all is well, i.e. I get a typescript error on the last line correctly stating that the type of name of the object I'm passing in is wrong.
interface Photo {
id: number;
name: string;
}
const photos: { [key: number]: Photo } = {
1: { id: 1, name: "photo-1" },
2: { id: 2, name: "photo-2" }
};
const extendedPhotos = extendWith<Photo>(photos, { id: 4, name: 3 });
Now, when I remove the explicit parameter <Photo> in the extendWith call on that last line, the typescript error disappears. I assume this has to do with typescript generic inference.
Does anyone know a way to achieve the inference being correct? Any tips to send me on the right path are much appreciated!
A sandbox to play with available here.
Firstly, It looks like your first overload is unnecessary, as you're saying that when the id is a string or a number, the returned object is either Partial<O>, or a full O. O will always be valid when mapped to a Partial<O> type, so you can just say its type is Partial<O>.
With regards to the inference, if you let TypeScript infer the type it will use your input to infer the output of the function. What it seems you are asking for is that the function's second argument MUST be of type Photo, which isn't inference, and can't be inferred unless you pass it a variable that is of type Photo already. For TS to infer, you'd need to replace your last line with something like:
const myPhoto: Photo { id: 4, name: 'my-photo' };
const extendedPhotos = extendWith<Photo>(photos, myPhoto);
So that TypeScript can use the information from the input values to infer the output value.
This can be done (though this example will need extending):
type Index = string | number;
type ObjectWithIdIndex = { id: Index };
function extendWith<O extends Record<Index, ObjectWithIdIndex>>(obj: O, v: O[keyof O]): O {
(obj as Record<Index, ObjectWithIdIndex>)[v.id] = v;
return obj;
}
The types of these parameters can be reduced to:
requiring that obj is an object with values that have id properties. We can constrain this by only accepting types that extend Record<Index, ObjectWithIdIndex>. So the following should give a type error:
extendWith(null, { id: 3, name: "photo-4" });
extendWith({ abc: 1 }, { id: 3, name: "photo-4" });
requiring that v is of the same type as of the values of obj. We can constrain this with O[keyof O]. As keyOf is a union of the properties of obj, O[keyof O] is a union of the values of these properties. The following should also give a type error:
interface Photo { id: number; name: string; }
const photos: Record<number, Photo> = {
1: { id: 1, name: "photo-1" },
2: { id: 2, name: "photo-2" }
};
extendWith(photos, { id: 4, name: "photo-4", abc: 2 });
extendWith(photos, { id: 4, name: 1 });
extendWith(photos, { name: "abc" });
extendWith(photos, null);
When calling extendWith, typescript will infer a more specific type for obj than Record<Index, ObjectWithIdIndex>. This has the consequence that:
We can use this type to infer the types of obj's values and then constrain v.
We can no longer assign new properties to obj as typescript doesn't know if obj is still an extensible Record type (eg. { a: 1 } is a subtype of Record<string, number>, but new properties can't be assigned to it). We can however cast obj back to its more generic type and then extend it.
I'm struggling to get over an error on TS.
I define value (below in the code) according 2 interfaces that I created (WalletInformationsEdit and UserInformationsEdit)
The problem that I encounter is at the line right after the DB query, value[field] is underlined saying :
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'WalletInformationsEdit | UserInformationsEdit'.
No index signature with a parameter of type 'string' was found on type 'WalletInformationsEdit | UserInformationsEdit'.
I have find 2 solutions to avoid this error message, but I am not ok because they make my code less protected:
1/ In TS config, if I take off "strict": true and "noImplicitAny": true -> working
2/ if I define value with "any" type, -> working
But both are not worth it, I suppose.
Do you have any recommandation to handle this case ?
Thanks in advance,
Paul
public async update(value: WalletInformationsEdit | UserInformationsEdit): Promise<any> {
try {
// saving id before elem treatment
const keepId = value.id
delete value.id
let filterFields = []
for (let elem of Object.keys(value)) {
filterFields.push(elem)
}
let i = 0;
let updateList = [];
for (const field of filterFields) {
updateList.push(`"${field}" = $${++i}`);
}
const preparedQuery = {
text: `UPDATE "${this.constructor.name.toLowerCase()}" SET
${updateList.join()}
WHERE id = $${++i}
RETURNING *
`,
values: [...filterFields.map((field) => value[field]), keepId],
};
const result = await db.query(preparedQuery);
return result.rows[0]
} catch (error) {
throw new Error(error.message)
}
}
WalletInformationsEdit and UserInformationsEdit interfaces
export interface UserInformationsEdit {
id?: number,
email?: string,
password?: string,
country?: string
}
export interface WalletInformationsEdit {
id?: number,
name?: string,
is_default?: boolean,
}
I finally find the answer, to declare the index signature in my case, i had to declare it this way
export interface WalletInformationsEdit {
[key: string]: number | string | boolean | undefined;
id?: number,
name?: string,
is_default?: boolean
}
[key: string] -> index is read as a string
: number | string | boolean | undefined -> each type that composed my interface + undefined because properties are optional
thanks #DigitalDrifter for the link :-)