TS2322: Type 'T' is not assignable to type 'never' - typescript-generics

I'm porting some JS to TS and have a simple app state module which allows me to set a property for later retrieval. When the ts compiler reaches the line state[propertyName] = value; inside the setAppState function, it throws the following error
TS2322: Type 'T' is not assignable to type 'never'.
I searched for this error but none of the other answers seem to address this case where I'm setting an object property using bracket notation.
Here's my code...
export interface appState {
context: typeof cast.framework.CastReceiverContext,
senderId: string,
requestId: number,
sessionId: string,
}
const state: appState = {
context: null,
senderId: null,
requestId: null,
sessionId: null,
};
export const getAppState = (propertyName: keyof appState): keyof appState => {
return state[propertyName];
};
export const setAppState = <T>(propertyName: keyof appState, value: T): void => {
state[propertyName] = value;
};

I found another question here that showed the correct way to write the types in the function signatures for the generic getter and setter, which solves the problem. The following now compiles.
export const getAppState = <K extends keyof appState>(propertyName: K): appState[K] => {
return state[propertyName];
};
export const setAppState = <K extends keyof appState>(propertyName: K, value: appState[K]): void => {
state[propertyName] = value;
};

Related

Argument of type 'Function | FieldValue' is not assignable to parameter of type 'string'

I create a user model:
class UserModel extends Model {
static table = 'users'
static timestamps = true
static fields = {
id: { primaryKey: true, autoIncrement: true},
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
username: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING,
birthday: DataTypes.DATE,
phoneNumber: DataTypes.INTEGER,
}
}
while I compare the existent user password with the new one:
async signin(user: Pick<User, "username" | "password">){
const { username, password } = user;
const existentUser = await UserModel.where('username', username).first()
if (!existentUser) throw new CustomErrorHandler(Status.NotFound, "User does not exist")
const isPasswordCorrect = await bcrypt.compare(password, existentUser.password); // Argument of type 'Function | FieldValue' // is not assignable to parameter of type 'string'.
// Type 'null' is not assignable to type 'string'.
}
I got this ts error:
Argument of type 'Function | FieldValue' is not assignable to parameter of type 'string'.
Type 'null' is not assignable to type 'string'.
I can fix it by forcing the type using :
const isPasswordCorrect = await bcrypt.compare(password, <string>existentUser.password);
but I'm looking for another solution. Is there another approach to convert returned Model by first() to User interface or something else?
In order to use typed fields on your models, you'll want to set up Model Records by duplicating the fields, like so:
class UserModel extends Model {
static table = 'users'
static timestamps = true
static fields = {
id: { primaryKey: true, autoIncrement: true},
firstName: DataTypes.STRING,
lastName: DataTypes.STRING,
username: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING,
birthday: DataTypes.DATE,
phoneNumber: DataTypes.INTEGER,
}
id!: string;
firstName!: string;
lastName!: string;
username!: string;
email!: string;
password!: string;
birthday!: Date;
phoneNumber!: number;
}
Also, it looks like /x/denodb doesn't set up generic return types on the model functions. I was able to get your example to typecheck by only casting the return of first() to UserModel. Here's the version of signin() that I was running myself:
async function signin(user: Pick<UserModel, "username" | "password">){
const { username, password } = user;
const existentUser = <UserModel> await UserModel.where('username', username).first();
if (!existentUser) throw new Error("User does not exist")
const isPasswordCorrect = bcrypt.compareSync(password, existentUser.password);
if (!isPasswordCorrect) throw new Error("Bad password")
}
Needing to cast to the correct model seems like something that can be improved in /x/denodb. I'll see if the project accepts a PR, but for now the above seems clean enough :)

When fetching data in a firebase cloud function from firestore we get is not assignable errors when using a converter

I am writing a cloud function that needs to retrieve a document from firestore using get(). I then need to use this data, so I am trying to use a converter so I can use the object later on down the line. The converter uses FamilyRequest to specify fields. Here is the relevant code:
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import * as firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
admin.initializeApp();
const db = admin.firestore();
const settings = {timestampsInSnapshots: true};
db.settings(settings);
class FamilyRequest {
recipient;
sender;
familyId;
role;
senderIsModGuard;
constructor (recipient: string, sender: string, familyId: string, role: string, senderIsModGuard: boolean) {
this.recipient = recipient;
this.sender = sender;
this.familyId = familyId;
this.role = role;
this.senderIsModGuard = senderIsModGuard;
}
toString() {
return this.recipient + ', ' + this.sender + ', ' + this.familyId + ', ' + this.role + ', ' + this.senderIsModGuard;
}
}
const familyRequestConverter = {
toFirestore(famrequest: FamilyRequest): firebase.firestore.DocumentData {
return {
recipient: famrequest.recipient,
sender: famrequest.sender,
familyId: famrequest.familyId,
role: famrequest.role,
senderIsModGuard: famrequest.senderIsModGuard
};
},
fromFirestore(
snapshot: firebase.firestore.QueryDocumentSnapshot,
options: firebase.firestore.SnapshotOptions
): FamilyRequest {
const data = snapshot.data(options)!;
return new FamilyRequest(data.recipient, data.sender, data.familyId, data.role, data.senderIsModGuard);
}
};
export const acceptFamilyRequestFromFamilyModerator = functions.https.onCall(
async (data, context) => {
// Message text passed from the client.
if (data && context && context.auth && context.auth.uid) {
const groupId = data.text.groupId;
// Authentication info is automatically added to the request.
const uid = context.auth.uid;
const requid = data.text.uid;
const famrequestrecipient = data.text.famrequestrecipient;
const famrequestsender = data.text.famrequestsender; //this is the original sender of the famrequest
//Do sender/recipient checks here
const ref = await db.collection("famrequests")
.doc(uid)
.collection('myfamrequests')
.doc(famrequestrecipient)
.withConverter(familyRequestConverter)
.get()
.then(doc => { //This is simplified for now
if (doc.exists) {
return doc;
} else {
return null;
}
});
}
});
When entering this code into the IDE, the following errors come up:
No overload matches this call.
Overload 1 of 2, '(converter: null): DocumentReference', gave the following error.
Argument of type '{ toFirestore(famrequest: FamilyRequest): DocumentData; fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): FamilyRequest; }' is not assignable to parameter of type 'null'.
Overload 2 of 2, '(converter: FirestoreDataConverter): DocumentReference', gave the following error.
Argument of type '{ toFirestore(famrequest: FamilyRequest): DocumentData; fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): FamilyRequest; }' is not assignable to parameter of type 'FirestoreDataConverter'.
Types of property 'fromFirestore' are incompatible.
Type '(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) => FamilyRequest' is not assignable to type '(snapshot: QueryDocumentSnapshot) => FamilyRequest'.ts(2769)
const familyRequestConverter: {
toFirestore(famrequest: FamilyRequest): firebase.firestore.DocumentData;
fromFirestore(snapshot: firebase.firestore.QueryDocumentSnapshot, options: firebase.firestore.SnapshotOptions): FamilyRequest;
}
I am not entirely sure what I am doing wrong as I am following the example code for converters here:
https://firebase.google.com/docs/firestore/query-data/get-data#web-version-8_3
very closely.
Any help is much appreciated.
I made the following change to familyRequestConverter that worked:
const familyRequestConverter = {
toFirestore: function(famrequest: FamilyRequest): firebase.firestore.DocumentData {
return {
recipient: famrequest.recipient,
sender: famrequest.sender,
familyId: famrequest.familyId,
role: famrequest.role,
senderIsModGuard: famrequest.senderIsModGuard,
};
},
fromFirestore: function(
snapshot: firebase.firestore.DocumentData
) {
const data = snapshot.data()!;
return new FamilyRequest(data.recipient, data.sender, data.familyId, data.role, data.senderIsModGuard);
}
};
This overloading error can be cause if you are missing either toFirestore or fromFirestore functions in the converter object which is passed to withConverter function.

firebase snapshot.val() returns null

I'm trying to get user information from Firebase database, but it always returns null.
export const employeesFetch = () => {
const { currentUser } = firebase.auth();
return (dispatch) => {
firebase.database().ref(`/users/${currentUser.uid}/employees`)
.on('value', snapshot => {
console.log("snapshot: ", snapshot.val()) // null
dispatch({ type: EMPLOYEE_FETCH_SUCCESS, payload: snapshot.val() });
});
};
};
If you use numeric IDs for an array of objects, then it will fill in missing items with a null. For example if you don't have an element 0 then your first item will be null.
{
"myList" : [ null, null, {
"myProp" : "thing",
"created" : 1582242432,
}, {
"myProp" : "another",
"created" : 1582242555,
}
}
Why do firebase collections seem to begin with a null row?
According to the documentation, val() returns null when there's no data at the location of the query:
Depending on the data in a DataSnapshot, the val() method may return a scalar type (string, number, or boolean), an array, or an object. It may also return null, indicating that the DataSnapshot is
empty (contains no data).
The internal contents of the DataSnapshot object are not to be used directly. Use the public API instead.

Flow: how to extend native JS object (especially Promise)?

// #flow
type Deferred = Promise<any> & {
reject: Function;
resolve: Function;
};
/** Deferred based on Promise
#return {Promise} */
export default (): Deferred => {
let res, rej;
let deferred: Deferred = Object.assign(
new Promise( ( resolve, reject ) => {
res = resolve;
rej = reject;
}),
{ 'resolve': res, 'reject': rej }
);
return deferred;
};
Right now this code gives such errors:
Cannot assign Object.assign(...) to deferred because:
property reject is missing in Promise [1] but exists in object type [2].
property resolve is missing in Promise [1] but exists in object type [2].
Property reject is missing in Promise [1].
Property resolve is missing in Promise [1].
And the question is: how to correctly document this code without errors?
It's not a good idea to use any.
It's not a good idea to use Function because it's just an alias for any.
It's better not to use &: it's broken. Use type spread instead.
If you really want to extend Promise, just extend it like this https://flow.org/try/#0PTAEAEDMBsHsHcBQBjaBDAzh0ARAppHgE5F4AmAPACoB8oeAHgC54B2Z2ACkbALYCWGPNToBvRKFABiUhljQAbngBcoABRUAlKAC8dfIRLkRoAD6gFsfmQDcE6aQBWeZE1VqBDctr24CxUkpaMwsrW3t7ZFhWDCYiAFdXWCI1SFYAfndZeSVM9S1dOk9yABpQJxcmPI9+LzIfItrvQtDrbXFJSSiYplAABx4GAE9dUFEAXztO0Ax4vuI1NWzFPDKK1wax+2mB2GGAOmWlUaO8Ken+waHDvGdXE9vK8+n+SHU07TSlvDkVtceNs9QONNECmAALQT7GQ-HJ4Ua7A6nMGQjDQ9a9HSXPbXDHncYRSSnb6zaBuUBaVQGALGYIdToQqEw35KEnxMmg7blPBMeJEVigRkYfGIewYtQBZKqYr1Kn+IxBMRcoXogFMCUkZKc6akXn8wWokWSRCTUXdWKgMgEUasPDwPyGQJqTmIECgeDJADWGEQVsgNxZeDUAEZNPsIWxFgxVKx4rwAEbETbm+R4fZwADmagYmk5bqiRlc0BGGHBCGwaAFkqIvoIAbhIbDEdYUdUsSI-FYGeT0V+acz2dzdkQjD6yV6frQ7N61IVdiAA
class Deferred<T> extends Promise<T> {
#resolve: (T) => Deferred<T> | void;
#reject: (mixed) => Deferred<T> | void;
constructor(fn?: (resolve?: (T) => mixed, reject?: (mixed) => mixed) => void) {
const proxy = {};
super((resolve, reject) => {
proxy.resolve = resolve;
proxy.reject = reject;
if (fn) fn(resolve, reject);
});
this.#resolve = proxy.resolve;
this.#reject = proxy.reject;
}
resolve(result: T): Deferred<T> {
this.#resolve(result);
return this;
}
reject(error: mixed): Deferred<T> {
this.#reject(error);
return this;
}
};

Best practice redux actions with flowtype?

I want to write redux with flowtype, but I have questions about how to do that.
Redux with Flowtype
type ActionType =
| 'A'
| 'B'
;
// A generic type conforming to Flux Standard Action
type ActionT<A, P> = {|
type: A,
payload?: P | Error,
error?: boolean,
meta?: mixed
|};
type Action =
| ActionT<'A', string>
| ActionT<'B', number>
;
const error: Error = new Error('wrong');
const info = { href: '...' };
// ---- valid actions with flowtype ----
const action1: Action = {type: 'A', payload: 'hello' };
const action2: Action = {type: 'A', payload: error, error: true }; // The 'payload' could be an error.
const action3: Action = {type: 'A', payload: 'hello', meta: info }; // We can have 'meta'.
// ---- invalid actions with flowtype ----
const actionNG1: Action = {type: 'C', payload: 'hello' }; // Wrong 'type' value. The type 'C' is not allowed.
const actionNG2: Action = {type: 'B', payload: 'hello' }; // Wrong value type of 'payload'. It should be a number.
const actionNG3: Action = {type: 'A', payload: 'hello', threshold: 3 }; // Extra property 'threshold' is not allowed. It should conform to type ActionT.
I use ActionType instead of constants to check the valid type values.
The type ActionT conforms Flux Standard Action to ensure the structure of Redux actions.
The type Action declares the concrete types for all actions that we'll use in our App.
Question 1: how to ensure the first type passed to ActionT would be the type of ActionType (or at least, should be a string type)?
For example, adding a new type 'C' in not allowed, because ActionType only accepts 'A' and 'B'.
type ActionType =
| 'A'
| 'B'
;
type Action =
| ActionT<'A', string>
| ActionT<'B', number>
| ActionT<'C', number> // Should Raise an error
;
Does it make sense?
Question 2: how to write the reducers (and thunk) with flowtype and Immutable.js?
I've written a buildReducer to bind to a initial state.
type Action =
| ActionT<'SELECT_COUNTRY', string>
;
const buildReducer = (initialState, reducerMap) =>
(state = initialState, action) => {
const reducer = reducerMap[action.type];
return (reducer) ? reducer(state, action) : state;
};
const initialState = Immutable.fromJS({
selectedCountry: 'US'
})
const reducers = {
['SELECT_COUNTRY']: (state, action) => {
// how to know the type of payload is string type?
// how to ensure that `state.set(...)` gets right type of the value for the key?
return state.set('selectedCountry', action.payload)
}
}
const appReducer = buildReducer(initialState, reducers)
How can I check the payload type in the reducers for action type SELECT_COUNTRY with flowtype ?
How can I apply the payload value to the immutable state with flowtype check?
To answer question 1, you can use bounded polymorphism
// A generic type conforming to Flux Standard Action
type ActionT<A: ActionType, P> = {|
type: A,
payload?: P | Error,
error?: boolean,
meta?: mixed
|};
Notice how we bound the generic parameter A to ActionType
To answer question 2, you have to make sure to pull in immutable-js types here: https://github.com/facebook/immutable-js/blob/master/type-definitions/immutable.js.flow and then type your reducer functions.

Resources