Typescript - How to get a subset of properties from an object into a variable based on an interface - reactjs

I have a function that receives an intersection of two types:
interface A {
propA: string;
}
interface B {
propB: number;
propC: boolean;
}
function C(props: A & B) {
}
Now, inside the function body I would like to get objects containing only the subset of properties from each of the interfaces; so I was wondering if Typescript had any utility to achieve that:
function C(props: A & B) {
const a = fancyPicker<A>(props);
const b = fancyPicker<B>(props);
console.log(a);
// prints "{ propA: "some string" }"
console.log(b);
// prints "{ propB: 42, propC: false }"
}

You're after a function that iterates over a known set of property names - this can't be done in pure TypeScript because TypeScript uses type-erasure, so the runtime script has no-knowledge of what the set of property names is.
But using a TypeScript compile-time extension known as a Custom Transformer, specifically ts-transformer-keys the TypeScript compiler will emit property-name lists that can be used.
Here's something that works, but isn't perfect because it doesn't use the types of each property - it only matches names:
import { keys } from 'ts-transformer-keys'; // <-- This module is where the magic happens.
type IndexableObject = {
[key: string]: any
};
/*
* The `IndexableObject` above is a hack. Note it allows `any` property type. Ideally it'd be something like this instead, but using `keyof` in a type indexer is not yet supported: https://github.com/microsoft/TypeScript/pull/26797
*
type IndexableObject<TOther> = {
[key: TKey extends keyof TOther]: PropertyType<TOther,TKey>
};
*/
function fancyPicker<TSubset extends object>(superset: IndexableObject): Partial<TSubset> {
const subsetPropertyNames = keys<TSubset>();
const ret: Partial<TSubset> = {
};
for (const subsetPropertyName of subsetPropertyNames) {
const propName: string = subsetPropertyName as string; // <-- This is also a hack because property keys/names are actually `string | number | symbol` - but this function assumes they're all named properties.
if (propName in superset) {
const value = superset[propName];
ret[subsetPropertyName] = value;
}
}
return ret;
}
Usage (using your example):
interface A {
propA: string;
}
interface B {
propB: number;
propC: boolean;
}
type ABIntersection = A & B;
type ABUnion = A | B;
function C(props: ABIntersection) {
const a = fancyPicker<A>(props);
const b = fancyPicker<B>(props);
console.log(a);
// prints "{ propA: "some string" }"
console.log(b);
// prints "{ propB: 42, propC: false }"
}
const testValue = { propA: "some string", propB: 42, propC: false, propD: "never see this" };
C(testValue);

Related

Cannot augment/extend generic type from my own library, when it's imported in a project?

Following up with two of my other questions. I'm working on a library that also depends on a lot of user-provided values. So for its type system to work, there must be some sort of type augmentation.
Styled components for example, does this with module augmentation by allowing the user to create a declarations file.
But I'm having a hard time having this to work in my implementation:
(please note the Library package and Main package comments)
/*
* ========================
* Library package
* ========================
*/
/*
* ========================
* Control
* ========================
*/
type ControlSchema<
ControlName extends string,
// This is used for inference on MapControlTypes hence why it's unused here
Type = any
> = {
name: ControlName
};
type Control = AllControls;
// This should return the names of ALL controls
type ControlNames = Control['name'];
// Converts "Test 1" to "number", "Test 2" to "string"
type MapControlTypes<Name extends ControlNames, C extends Control = Control> = C extends ControlSchema<Name, infer Type> ? Type : never;
const buildControlSchema = <ControlName extends string, Type, C extends ControlSchema<ControlName, Type> = ControlSchema<ControlName, Type>>(c: C) => c;
/*
* ========================
* Block
* ========================
*/
type ControlDeclaration<C extends Control = Control> = C extends ControlSchema<infer ControlName> ? { name: ControlName } : never;
type BlockControl = {
[key: string]: ControlDeclaration;
};
type BlockSchema<BC extends BlockControl> = {
title: string;
controls: BC;
};
// Ref: https://stackoverflow.com/questions/71785211/how-to-infer-types-of-an-object-created-from-a-schema
type InferBlockPropsFromControl<BS> = BS extends BlockSchema<infer BC> ? { [P in keyof BC]: MapControlTypes<BC[P]['name']> } : never;
// Ref: https://stackoverflow.com/questions/71785211/how-to-infer-types-of-an-object-created-from-a-schema
const buildBlockSchema = <BC extends BlockControl, B extends BlockSchema<BC> = BlockSchema<BC>>(b: B) => b
/*
* ========================
* Main package (the one that imports the library package)
* ========================
*/
const firstControl = buildControlSchema<'Test 1', number>({
name: 'Test 1'
})
const secondControl = buildControlSchema<'Test 2', string>({
name: 'Test 2'
})
const controls = [
firstControl,
secondControl
]
// This must be fed back into Library package
type AllControls = typeof controls[number];
var firstBlock = buildBlockSchema({
title: "First block",
controls: {
testOne: {
name: 'Test 1'
},
testTwo: {
name: 'Test 2'
}
}
});
var secondBlock = buildBlockSchema({
title: "Second block",
controls: {
second: {
name: 'Test 2'
},
block: {
name: 'Test 1'
}
}
});
const blocks = [
firstBlock,
secondBlock
]
// This must be fed back into Library package
type AllBlocks = typeof blocks[number];
type NewSchemaProps = InferBlockPropsFromControl<typeof firstBlock>;
/* Expected result:
NewSchemaProps = {
testOne: number;
testTwo: string;
}
*/
The problem is... It works fine through the typescript playground (look at lines 79, 82, 107 how the autocomplete works and also inferred typing), but as soon as I install my library as a package. It doesn't work compared to the playground...
Playground(Does work):
Package(Doesn't work):
If it helps, I have set up an example repo reproducing the issue.
In your typescript playground, you've defined the type of AllControls only after creating your control array
type Control = AllControls;
// This should return the names of ALL controls
type ControlNames = Control['name'];
...
// This must be fed back into Library package
type AllControls = typeof controls[number];
In the repository, you only declare MyControls but you haven't overwritten AllControls and you can't once the type is officially declared
export const controls = [firstControl, secondControl];
export type MyControls = typeof controls[number];
There may be a way to pass control types to buildBlockSchema<typeof MyControls,..>({})

Use typescript to create object from array of objects

I have the following typescript code:
const a = [{ foo: 1 }, { bar: '2' }]
I wish to use a to create an object of the form:
const b = {
foo: 1,
bar: '2'
}
and the typing of b should be equivalent to the type:
type EquivalentType = {
foo: number
bar: string
}
Is this possible without casting? How can this be done?
Sure there is. This solution does not need as const like #Vija02's (although it is perfectly fine if it does).
Map over all possible keys in the array, then get only the type of that key using Extract:
type CreateFrom<T extends ReadonlyArray<unknown>> = { [K in keyof T[number]]-?: Extract<T[number], { [_ in K]: any }>[K] };
Then you'd just use this type in a supposed function:
function createFrom<T extends ReadonlyArray<unknown>>(list: T): CreateFrom<T> {
// ... for you to implement!
}
Note that you might need to cast the return type. I don't think TypeScript will be too happy with this one.
And to finish it off, this is a playground demonstrating the solution.
// You might be able to simplify this
type TypeFromLiteral<T> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : never;
// The "as const" part is important so that we can process the types
const a = [{ foo: 1 }, { bar: '2' }] as const;
// Get the final type
type ObjectUnion = typeof a[number];
type NewType = { [T in ObjectUnion as keyof T]: TypeFromLiteral<T[keyof T]> };
// By itself, this will get the correct value. However, we need to process the type separately and cast it to get what you want.
const b = Object.assign({}, ...a) as NewType;

Typescript: problem with intersection of types containing a subset of specialized arrays

I've a problem with intersection of types containing array members whose type is a subset of a specific type. It's harder to explain for me than simply show you the code.
This is a custom type
type MyType = 'A' | 'B' | 'C' | 'D';
This is the generic interface for the "base" object
interface Base<A = {}, B = {}> {
stringMember: string;
arrayMember: MyType[];
objectMember: {
something: boolean;
} & A;
b: B;
}
I need to specialize arrayMember, objectMember and b memebers like so
type A_Object_1 = {
somethingElse: number;
};
type B_Object_1 = {
aMember: boolean;
};
// Specialized type 1
type Specialized_1 = Base<A_Object_1, B_Object_1> & {
arrayMember: ['A'];
};
// Specialized type 1 object, this is OK
const a_OK: Specialized_1 = {
stringMember: 'hi',
arrayMember: ['A'],
objectMember: {something: true, somethingElse: 3},
b: {aMember: true},
};
and so
type A_Object_2 = {
anything: boolean;
};
type B_Object_2 = {
bMember: number;
};
type Specialized_2 = Base<A_Object_2, B_Object_2> & {
arrayMember: ['B'];
};
// Specialized type 2 object, this is OK
const b_OK: Specialized_2 = {
stringMember: 'hello',
arrayMember: ['B'],
objectMember: {something: true, anything: true},
b: {bMember: 3},
};
I need an intersection type between Specialized_1 and Specialized_2, it should be like this
{
stringMember: string;
arrayMember: ['A', 'B'];
objectMember: {
something: boolean;
somethingElse: number;
anything: boolean;
};
b: {aMember: boolean; bMember: number};
}
I do that the following (maybe wrong) way
type Specialized_3 = Specialized_1 & Specialized_2;
The members that are objects are ok, since they contain both members from Specialized_1 & Specialized_2
Anyway there is a problem with the array type that is never, I see why, there is no intersection between ['A'] and ['B'],
const c_Problem: Specialized_3 = {
stringMember: 'hi there',
arrayMember: ['A', 'B'], // Error here because arrayMember: never
objectMember: {something: true, somethingElse: 3, anything: false}, // OK
b: {aMember: false, bMember: 8}, // OK
};
It's been 2 days I'm thinking about that and I can't figure it out...
Moreover I find difficult to find the search keywords for this very specific problem.
Any help?
EDIT:
Thanks to #AlekseyL.I made some progress, I can use Omit to omit arrayMember from the intersected object and than union it later "manually"
I can do this using a generic type like so:
type PossibleSolution<T1 extends Base, T2 extends Base> = Omit<T1 & T2, 'arrayMember'> & {
arrayMember: [T1['arrayMember'][number], T2['arrayMember'][number]];
};
type Specialized_Test = PossibleSolution<Specialized_1, Specialized_2>;
const d_OK: Specialized_Test = {
stringMember: 'hi there',
arrayMember: ['A', 'B'], // OK
objectMember: {something: true, somethingElse: 3, anything: false}, // OK
b: {aMember: false, bMember: 8}, // OK
};
The use of the generic type is for convenience since I need to reuse that here and there in my code.
This solution works, but only to intersect 2 objects, if I need to intersect 3 objects I have to delcare a different generic type
Now the problem is, how can I modify PossibileSolution generic type to intersect N SpecializedObjects?

Extending Array in TypeScript for specific type

I know how to extend array for any type:
declare global {
interface Array<T> {
remove(elem: T): Array<T>;
}
}
if (!Array.prototype.remove) {
Array.prototype.remove = function<T>(this: T[], elem: T): T[] {
return this.filter(e => e !== elem);
}
}
Source: Extending Array in TypeScript
But is there also a way to extend the array only for a specific type?. Like only for arrays of type User -> Array<User>.
I want to create a extend method, like .toUsersMap() and this method should not be displayed for arrays, which have not the type User.
You can achieve similar behaviour:
type User = {
tag: 'User'
}
interface Array<T> {
toUsersMap: T extends User ? (elem: T) => Array<T> : never
}
declare var user: User;
const arr = [user]
arr.toUsersMap(user) // ok
const another_array = [{ notUser: true }]
another_array.toUsersMap() // This expression is not callable
If T parameter does not extends User, TS will disallow using toUsersMap
Playground
I don't think there's a way to completely suppress the IntelliSense prompting for toUsersMap() on Arrays, but you can definitely make it a compiler error to call arr.toUsersMap() unless arr is assignable to Array<User>. One way to do this is to give the toUsersMap() method a this parameter:
interface Array<T> {
toUsersMap(this: Array<User>): Map<string, User>;
}
Now the compiler will require that toUsersMap() only be called with a this context of something assignable to Array<User>:
interface User {
username: string;
}
const arr = [{ username: "foo" }, { username: "bar" }];
arr.toUsersMap() // okay, no error
const another_array = ["hello", 123];
another_array.toUsersMap() // error,
//~~~~~~~~~~~ <--
// The 'this' context of type '{ notUser: boolean; }[]' is
// not assignable to method's 'this' of type 'User[]'
Playground link to code

TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'rowType' [duplicate]

When using Object.keys(obj), the return value is a string[], whereas I want a (keyof obj)[].
const v = {
a: 1,
b: 2
}
Object.keys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, []);
I have the error:
Element implicitly has an 'any' type because type '{ a: number; b: number; }' has no index signature.
TypeScript 3.1 with strict: true. Playground: here, please check all checkboxes in Options to activate strict: true.
Object.keys returns a string[]. This is by design as described in this issue
This is intentional. Types in TS are open ended. So keysof will likely be less than all properties you would get at runtime.
There are several solution, the simplest one is to just use a type assertion:
const v = {
a: 1,
b: 2
};
var values = (Object.keys(v) as Array<keyof typeof v>).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
You can also create an alias for keys in Object that will return the type you want:
export const v = {
a: 1,
b: 2
};
declare global {
interface ObjectConstructor {
typedKeys<T>(obj: T): Array<keyof T>
}
}
Object.typedKeys = Object.keys as any
var values = Object.typedKeys(v).reduce((accumulator, current) => {
accumulator.push(v[current]);
return accumulator;
}, [] as (typeof v[keyof typeof v])[]);
Based on Titian Cernicova-Dragomir answer and comment
Use type assertion only if you know that your object doesn't have extra properties (such is the case for an object literal but not an object parameter).
Explicit assertion
Object.keys(obj) as Array<keyof typeof obj>
Hidden assertion
const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>
Use getKeys instead of Object.keys. getKeys is a ref to Object.keys, but the return is typed literally.
Discussions
One of TypeScript’s core principles is that type checking focuses on the shape that values have. (reference)
interface SimpleObject {
a: string
b: string
}
const x = {
a: "article",
b: "bridge",
c: "Camel"
}
x qualifies as a SimpleObject because it has it's shape. This means that when we see a SimpleObject, we know that it has properties a and b, but it might have additional properties as well.
const someFunction = (obj: SimpleObject) => {
Object.keys(obj).forEach((k)=>{
....
})
}
someFunction(x)
Let's see what would happen if by default we would type Object.keys as desired by the OP "literally":
We would get that typeof k is "a"|"b". When iterating the actual values would be a, b, c. Typescript protects us from such an error by typing k as a string.
Type assertion is exactly for such cases - when the programmer has additional knowledge. if you know that obj doesn't have extra properties you can use literal type assertion.
See https://github.com/microsoft/TypeScript/issues/20503.
declare const BetterObject: {
keys<T extends {}>(object: T): (keyof T)[]
}
const icons: IconName[] = BetterObject.keys(IconMap)
Will retain type of keys instead of string[]
1. npm install ts-extras (written by sindresorhus)
Use it:
import { objectKeys } from 'ts-extras'
objectKeys(yourObject)
That's it.
====
Here's another pkg I made before I knew about ts-extras:
npm install object-typed --save
import { ObjectTyped } from 'object-typed'
ObjectTyped.keys({ a: 'b' })
This will return an array of type ['a']
I completely disagree with Typescript's team's decision...
Following their logic, Object.values should always return any, as we could add more properties at run-time...
I think the proper way to go is to create interfaces with optional properties and set (or not) those properties as you go...
So I simply overwrote locally the ObjectConstructor interface, by adding a declaration file (aka: whatever.d.ts) to my project with the following content:
declare interface ObjectConstructor extends Omit<ObjectConstructor, 'keys' | 'entries'> {
/**
* Returns the names of the enumerable string properties and methods of an object.
* #param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys<O extends any[]>(obj: O): Array<keyof O>;
keys<O extends Record<Readonly<string>, any>>(obj: O): Array<keyof O>;
keys(obj: object): string[];
/**
* Returns an array of key/values of the enumerable properties of an object
* #param obj Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T extends { [K: Readonly<string>]: any }>(obj: T): Array<[keyof T, T[keyof T]]>
entries<T extends object>(obj: { [s: string]: T } | ArrayLike<T>): [string, T[keyof T]][];
entries<T>(obj: { [s: string]: T } | ArrayLike<T>): [string, T][];
entries(obj: {}): [string, any][];
}
declare var Object: ObjectConstructor;
Note:
Object.keys/Object.entries of primitive types (object) will return never[] and [never, never][] instead of the normal string[] and [string, any][]. If anyone knows a solutions, please, feel free to tell me in the comments and I will edit my answer
const a: {} = {};
const b: object = {};
const c: {x:string, y:number} = { x: '', y: 2 };
// before
Object.keys(a) // string[]
Object.keys(b) // string[]
Object.keys(c) // string[]
Object.entries(a) // [string, unknown][]
Object.entries(b) // [string, any][]
Object.entries(c) // [string, string|number][]
// after
Object.keys(a) // never[]
Object.keys(b) // never[]
Object.keys(c) // ('x'|'y')[]
Object.entries(a) // [never, never][]
Object.entries(b) // [never, never][]
Object.entries(c) // ['x'|'y', string|number][]
So, use this with caution...
You can use the Extract utility type to conform your param to only the keys of obj which are strings (thus, ignoring any numbers/symbols when you are coding).
const obj = {
a: 'hello',
b: 'world',
1: 123 // 100% valid
} // if this was the literal code, you should add ` as const` assertion here
// util
type StringKeys<objType extends {}> = Array<Extract<keyof objType, string>>
// typedObjKeys will be ['a', 'b', '1'] at runtime
// ...but it's type will be Array<'a' | 'b'>
const typedObjKeys = Object.keys(obj) as StringKeys<typeof obj>
typedObjKeys.forEach((key) => {
// key's type: 'a' | 'b'
// runtime: 'a', 'b', AND '1'
const value = obj[key]
// value will be typed as just `string` when it's really `string | number`
})
All that said, most developers would probably consider having numbers as keys a poor design decision/bug to be fixed.
Here is a pattern I use for copying objects in a typesafe way. It uses string narrowing so the compiler can infer the keys are actually types. This was demonstrated with a class, but would work with/between interfaces or anonymous types of the same shape.
It is a bit verbose, but arguably more straightforward than the accepted answer. If you have to do the copying operation in multiple places, it does save typing.
Note this will throw an error if the types don't match, which you'd want, but doesn't throw an error if there are missing fields in thingNum. So this is maybe a disadvantage over Object.keys.
class thing {
a: number = 1;
b: number = 2;
}
type thingNum = 'a' | 'b';
const thingNums: thingNum[] = ['a', 'b'];
const thing1: thing = new thing();
const thing2: thing = new thing();
...
thingNums.forEach((param) => {
thing2[param] = thing1[param];
});
playground link
Here's a more accurate utility function:
const keys = Object.keys as <T>(obj: T) =>
(keyof T extends infer U ? U extends string ? U : U extends number ? `${U}` : never : never)[];
Explanation: keyof T extends string | number | symbol, however Object.keys omits the symbol keys and returns number keys as strings. We can convert number keys to string with a template literal `${U}`.
Using this keys utility:
const o = {
x: 5,
4: 6,
[Symbol('y')]: 7,
};
for(const key of keys(o)) {
// key has type 'x' | '4'
}
As a possible solution, you can iterate using for..in over your object:
for (const key in myObject) {
console.log(myObject[key].abc); // works, but `key` is still just `string`
}
While this, as you said, would not work:
for (const key of Object.keys(myObject)) {
console.log(myObject[key].abc); // doesn't!
}

Resources