Imagine having two data types, both of them have an id property:
type Foo = {
id: string,
// other props
}
type Bar = {
id: number,
// other props
}
Now imagine a Table component in React which accepts an array of data as a property. This component needs to be generic, but we know that it requires an id prop for every data object in the data array. This id can either be a number or a string. The props might be typed as follows:
type Props = {
data: Array<{id: number | string}>
}
The problem is that the above notation does not work. Is there a way to be able to 'retype' certain properties?
I have looked into generic types but I can't seem to find a solution without changing the Foo and Bar type definitions.
This is a bit too tricky for Flow to figure out I think. So instead of declaring a single property that both your types share, you can declare your array as containing types Foo | Bar.
This is nice because it has even less repetition than your original code, and in each branch of that if statement you have free access to other unique properties of Foo or Bar without any issues.
// #flow
type Foo = {
id: string,
// other props
}
type Bar = {
id: number,
// other props
}
type Props = {
data: Array<Foo | Bar>
}
const foo: Foo = { id: 'A' }
const bar: Bar = { id: 1 }
const props: Props = { data: [foo, bar] }
for (const item of props.data) {
if (typeof item.id === 'string') {
console.log('I am a Foo')
} else {
console.log('I am a Bar')
}
}
Try Flow Link
Related
I'm learning TypeScript, and decided to try implement it in a small portion of my codebase to get the ball rolling. Specifically, what I'm refactoring now is related to a fixture "factory" for the purpose of generating fixtures for Jest tests.
In addition to these factories, which spit out certain Objects, I also have some helper methods that make things like generating multiple objects a bit easier.
A factory is fairly simple, it looks something like this (the values are spoofed with faker.js):
function channelFactory(): ChannelItem {
return { foo: "bar" }
}
A ChannelItem is just a simple Object containing some keys
interface ChannelItem { foo: string; }
And as an example of one of those helper methods, I have a createMany function that takes in a Factory function and a Count as arguments
function createMany(factory: () => Record<string, unknown>, count = 1): Record<string, any>[] {
// A for loop that calls the factory, pushes those into an array and returns that array
}
However, if I try to use these factories somewhere, for example in this function that persists some created channels into the DB, I get the TS compiler warning me about Record<string, any>[] not being assignable to ChannelItem[].
function saveChannels(payload: ChannelItem[]): void { // Unimportant implementation details }
const items = createMany(channelFactory, 5);
saveChannels(items) // => Argument type Record<string, any>[] is not assignable to parameter type ChannelItem[] Type Record<string, any> is not assignable to type ChannelItem
I know this is a commonly known issue with Interfaces specifically (Issue #15300) and that the potential solution would be to declare a type rather than an interface, however in this situation I still get the same warning.
type ChannelItem = { foo: string } // Still gives me the above warning
What would the ideal way of making my factory functions more generic here be?
You could make the createMany function generic:
function createMany<K extends string, T>(factory: () => Record<K, T>, count = 1): Record<K, T>[] {
const arr = [];
for (let i = 0; i < count; i++) {
arr.push(factory());
}
return arr;
}
const items = createMany(channelFactory, 5);
console.log(items);
// Prints:
//[
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' },
// { foo: 'bar' }
//]
I made K extends string because you specified you want your record to have string keys. T can be anything you want.
Just have to fill in the functions yourself, not sure what you want done in those.
The createMany doesn't even need to know the type factory returns.
You can make it generic for more flexibility.
interface ChannelItem { foo: string; }
function channelFactory(): ChannelItem {
return { foo: "bar" }
}
function createMany<T>(factory: () => T, count = 1): T[] {
// A for loop that calls the factory, pushes those into an array and returns that array
return []
}
function saveChannels(payload: ChannelItem[]): void { }
const items = createMany(channelFactory, 5);
saveChannels(items)
TS playground
I'm trying to write a TS function which gets a nested value from given object. That object can be one of several types so I'm using generics. However, TS complains so I feel like I'm misunderstanding how generics work in TS:
interface BaseValueObject<T> {
value: T | null
}
type StringValue = BaseValueObject<string>
type NumberValue = BaseValueObject<number>
interface FormAData {
name: StringValue,
age: NumberValue
}
interface FormBData {
height: NumberValue
nickname: StringValue
}
interface FormA {
data: FormAData
}
interface FormB {
data: FormBData
}
type Form = FormA | FormB
const getFormValue =
<F extends Form, P extends keyof F['data']>(form: F, property: P) =>
form['data'][property]['value'] // Type 'P' cannot be used to index type 'FormAData | FormBData'
Desired usage:
const formARecord: FormA = {
data: {
name: {
value: 'Joe'
},
age: {
value: 50
}
}
}
const joesAge = getFormValue(formARecord, 'age')
console.log(joesAge) // 50
Playground
Solution
Here's what I ended up doing, similar to what #jered suggested in his answer:
playground
Basically the lesson is to make explicit any invariants you have your typings. In my case, I wasn't formally telling the compiler that every property FormAData and FormBData adhered to the same interface. I was able to do so by extending them from this base interface:
...
type Value = StringValue | NumberValue
interface BaseFormData {
[property: string]: Value
}
interface FormAData extends BaseFormData {
...
You should extend your declaration of generics to the "form" interfaces themselves.
In this case you need to give TypeScript a way to "infer" what the type of the data property of the form will be, in order for property to properly index it.
The way you have it written currently gives an error because you can't use keyof to extract the properties of a union type. Consider this example:
type Foo = {
fooProperty: string;
}
type Bar = {
barProperty: string;
}
type Baz = Foo | Bar;
type Qux = keyof Baz; // type Qux = never
What type is Qux supposed to be? It can't be the keys of two different types simultaneously, so it ends up being of type never which is not very useful for indexing properties of an object.
Instead, consider if your base Form type was itself a generic, that you know should always have a data property, but which the specific type of that data property is unknown until it is actually utilized. Fortunately, you could still constrain some aspects of data to ensure enforcements of its structure across your app:
interface FormDataType {
[key: string]: StringValue | NumberValue;
};
interface Form<T extends FormDataType> {
data: T
};
Then when you write your flavors of Form that have more specific type definitions for the data property, you can do so like this:
type FormA = Form<{
name: StringValue,
age: NumberValue
}>;
type FormB = Form<{
height: NumberValue
nickname: StringValue
}>;
In a way this is sort of like "extending" the type, but in a way that allows TypeScript to use the Form generic to infer (literally) the type of data later on.
Now we can rewrite the getFormValue() function's generic types to match the Form generics in the function signature. Ideally the return type of the function would be perfectly inferred just from the function parameters and function body, but in this case I didn't find a good way to structure the generics so that everything was seamlessly inferred. Instead, we can directly cast the return type of the function. This has the benefit of 1. still checking that form["data"] exists and matches the FormDataType structure we established earlier, and 2. inferring the actual type of the value returned from calling getFormValue(), increasing your overall type checking confidence.
const getFormValue = <
F extends Form<FormDataType>,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
Playground
Edit: on further reflection the generics of the Form interface itself is not really necessary, you could do something else like declare a basic Form interface and then extend it with each specific form:
interface FormDataType {
[key: string]: StringValue | NumberValue;
}
interface Form {
data: FormDataType
};
interface FormA extends Form {
data: {
name: StringValue;
age: NumberValue;
}
};
interface FormB extends Form {
data: {
height: NumberValue;
nickname: StringValue;
}
};
const getFormValue = <
F extends Form,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
I have a class that take an object as a constructor argument, and I want to enforce a method on the class to accept a very similar object. The objects have arbitrary keys.
For example, constructing with this object:
{
foo: { type: 'A' },
bar: { type: 'B'}
}
I will want the method to only accept objects of a similar form, i.e. has the same keys and for each key the value type is compatible with the initial object. like:
{
foo: SomeARelatedThing,
bar: SomeBRelatedThing
}
I've got a workaround in place where I can at least enforce the same keys, and then do lots of type checking (good to do anyway!) to make sure that the values actually match up.
Here's a contrived example from my use case:
type TypeName = 'A' | 'B' | 'C' // ...
class Action<K extends TypeName> {
type: K
constructor(type: K) { this.type = type }
}
type AnyAction = Action<'A'> | Action<'B'> | Action<'C'> // ...
type AProp = { type: 'A' }
type BProp = { type: 'B' }
type CProp = { type: 'C' }
type AnyProp = AProp | BProp | CProp // ...
type PropMap<K extends string> = Record<K, AnyProp>
type ActionMap<K extends string> = Record<K, AnyAction>
class Thing<K extends string> {
props: PropMap<K>
constructor(props: PropMap<K>) { this.props = props }
myMethod<>(actions: ActionMap<K>) { /* ... */ }
}
// type = Thing<'foo' | 'bar'>
const thing = new Thing({
foo: { type: 'A' },
bar: { type: 'B'}
})
// the keys are enforced, but how can the values of foo and bar be enforced, too
thing.myMethod({
foo: new Action('A'),
bar: new Action('B'),
})
I think I would want something more like a type equal to Thing<{foo: 'A', bar: 'B'}>, but I don't know how to conditionally compute that from a PropMap-like input to the Thing constructor, or even if I did, then how would I compute the correct ActionMap-like type.
MyMethod actually accepts a Partial<ActionMap<K>> but I don't think that that should matter for what I am asking.
I think I've got it. You need to use mapped types.
class Thing<K extends string, P extends PropMap<K>> {
props: P
constructor(props: P) { this.props = props }
myMethod(actions: {[Property in keyof P]: Action<P[Property]["type"]>}) { return actions }
}
const thing = new Thing({
foo: { type: 'A' },
bar: { type: 'B'}
})
// the keys are enforced, as well as the corresponding Action types
thing.myMethod({
foo: new Action('A'),
bar: new Action('B'),
})
Note that you need to add the generic P to your Thing class, otherwise TypeScript has no way of inferring more detailed information when you instantiate Thing later. Basically, you need to set P to be the same generic type consistently within Thing otherwise there is no way to differentiate it from the type PropMap which it extends.
Then, the magic happens in actions: {[Property in keyof P]: Action<P[Property]["type"]>}. Let's break it down:
[Property in keyof P]: mapped type index signature. This is what lets us get access to the specific keys in P, e.g. foo, bar etc.
Action<...> will set the value corresponding to each key we're mapping in (1.) above to some value, which we want to be an Action, like Action<'A'> etc. BUT we want the action to be derived from the original value of P, so...
P[Property]["type"] let's us access the value from the type key/value pair from the original type of P. Since Property varies (it's mapped from one type to the other) then for example it becomes foo: P["foo"]["type"] which is 'A', bar: P["bar"]["type"] which is 'B', etc
Playground
I'm starting to learning typescript applied to node JS backend.
For now i'm still using function and not class.
I used to write named function for each file like
const item= {
a:1,
b:2,
function1:()=>{
console.log(item.a)
},
function2:()=>{
console.log(item.b)
} }
then export it and use like item.function1. (i sometimes use it also as function1 with import as unstructured object)
Now using typescript i'm still using this approach but with types. The problem is that i can't assign a type to a because it's seen as value. I can't heither do :
const item= {
function1:()=>{
item.a = 3
console.log(item.a)
},
function2:()=>{
item.b = 4
console.log(item.b)
}}
because it's saying that property a or b does not exist in type item.
another thing i tried but that doesn't work is:
const item = {
function1:()=>{
item.a:number = 3
console.log(item.a)
},
function2:()=>{
item.b:number = 4
console.log(item.b)
} }
Anyone can help me? hoping that this named functions are not a bad habit for write code
When you declare a variable and don't specify its type, typescript will try to infer what its type is. When you assign an object, it will try to infer the type like this: look at what properties you wrote and consider that the object only has these properties with exactly the types that the object you wrote has. So in your last code example typescript will infer that item is of type
const item: {
function1: () => void
function2: () => void
} = {
// ...
}
You can see that it didn't add any other properties.
Next, if you don't declare a property when typing an object, typescript will think that this property doesn't exist and may not exist on the object. Consider this:
const obj: { foo: number } = {
foo: 6
}
obj.bar = 7 // Error
You didn't declare the bar property, so typescript doesn't allow to read it or assign something to it. This is why you cannot write item.a = 3 in your example: typescript didn't infer that item object has property a and it thinks that it must not exist
To solve this you just need to either assign all properties you will need when creating your object:
const item = {
a: 1,
b: 2,
function1: () => { /* ... */ },
function2: () => { /* ... */ },
}
Or type item manually
interface Item {
a?: number
b?: number
function1: () => void
function2: () => void
}
const item: Item = {
function1: () => {
item.a = 3
},
function2: () => {
item.b = 4
}
}
Note the question marks before the column inside the interface, this is to tell that these properties are optional. If you don't set these question marks, typescript will think these are obligatory, so it will emit an error if you create item and don't declare a and b properties
It is possible to denote item object as any.
To accomplish a desired consistency an interface could be defined as follows
interface ItemObject {
[key: string]: any
}
var item: ItemObject= {};
to make compact:
var item: {[k: string]: any} = {};
now item can accept any string as key and any type as value
Here is an object with several different key and value, and each props of value differ from each other, how to best describe this object using TypeScript? Especially the setValue method, how to limit type of the creatureType, prop and value?
const object = {
john: {
name: '',
age: 18
},
alien: {
height: 20,
power:100,
},
setValue(creatureType) {
const self = this
return function (prop) {
return function (value) {
self[creatureType][prop] = value
}
}
}
}
Your setValue() method will need to be generic if you want it to place strong restrictions on which properties and values go with which, uh, "creature type". Because the type of the object's setValue() method will be dependent on the type of the other properties of object, the compiler will give up trying to infer types for it; it's too circular for something that isn't a class. Either you could manually annotate all the types, which would be annoying, or you could split object into two pieces, say plainObject holding just the data, and then merge in the setValue() method which will be dependent on the type of plainObject, like this:
const plainObject = {
john: { name: '', age: 18 },
alien: { height: 20, power: 100 }
}
type PlainObject = typeof plainObject;
const object = {
...plainObject,
setValue<K extends keyof PlainObject>(creatureType: K) {
const self: PlainObject = this;
return function <P extends keyof PlainObject[K]>(prop: P) {
return function (value: PlainObject[K][P]) {
self[creatureType][prop] = value;
}
}
}
}
And you can verify that the compiler behaves as you want:
object.setValue("john")("age")(19); // okay
object.setValue("alien")("height")("pretty tall"); // error!
// "pretty tall" isn't numeric --> ~~~~~~~~~~~~~
object.setValue("john")("power")(9000); // error!
// "power" is wrong --> ~~~~~~~
object.setValue("elaine")("name")("elaine"); // error!
// "elaine"? -> ~~~~~~~~
Okay, hope that helps; good luck!
Link to code in Playground