Description
Do you want to request a feature or report a bug? Feature
(If this is a usage question, please do not post it here—post it on gitter. If this is not a “feature” or a “bug”, or the phrase “How do I...?” applies, then it's probably a usage question.)
What is the current behavior?
No type info for the schema of used database.
What is the expected behavior?
Moar typez.
Basic idea
The library provides an empty interface:
export interface FirebaseRtdbSchema { }
Developer has his own types for the schema. Not related to the lib in any way, he can do whatever he wants.
// some helpers
type RtdbUid<T> = string & { __UID_FOR__: T }
type FirebaseRecord<T> = Record<RtdbUid<T>, T>
type FirebaseKvPair<T> = { key: RtdbUid<T>, value: T }
type FirebaseKvPairs<T> = Array<FirebaseKvPair<T>>
interface RtdbUser {
avatarUrl: string
displayName: string
email: string
}
interface RtdbGameScore {
score: number
size: number
speed: number
user: RtdbUid<RtdbUser>
userDisplayName: string
}
interface RtdbSchema {
users: FirebaseRecord<RtdbUser>
gameScores: FirebaseRecord<RtdbGameScore>
}
Developer creates a .d.ts
file where he extends the existing typings by merging in his own schema:
// make react-redux-firebase aware of the schema, globally
import 'react-redux-firebase'
declare module 'react-redux-firebase' {
export interface FirebaseRtdbSchema extends RtdbSchema { }
}
Various methods and properties in the library can now use FirebaseRtdbSchema
to bring more type safety by having a generic fnName<T extends Record<string, any> = FirebaseRtdbSchema>
. If the developer doesn't provide anything, the globally declared schema is used; overriding mechanism allows for having multiple databases and such for advanced use cases.
Leveraging this means that the developer gets things like the following autocompletion.
Nested keys
When the lib requires specifying a path such as foo
, foo.bar
or foo/bar
, we can provide a simple .join('/')
function which has some magic types:
function chainOfKeys <
T extends Record<string, any> = Schema,
K1 extends keyof T = keyof T,
K2 extends (T[K1] extends Record<string, any> ? keyof T[K1] : never) = keyof T[K1],
K3 extends (T[K1][K2] extends Record<string, any> ? keyof T[K1][K2] : never) = keyof T[K1][K2],
K4 extends (T[K1][K2][K3] extends Record<string, any> ? keyof T[K1][K2][K3] : never) = keyof T[K1][K2][K3],
> (keys: [K1] | [K1, K2] | [K1, K2, K3, K4]) {
return keys.join('.')
}
Or, we can build this idea into the existing functions which accept such path. This will allow proper type checking of nested keys.
interface Schema {
one: string
two: {
two_one: string
}
three: {
three_one: {
three_one_one: {
three_one_one_one: string
three_one_one_two: string
}
}
}
}
chainOfKeys(['one'])
chainOfKeys(['one', 'two']) // oops
chainOfKeys(['two'])
chainOfKeys(['two', 'two_one'])
chainOfKeys(['three', 'three_one', 'three_one_one', 'tree_one_one_two']) // oops (can you see it?)
Query params
Similarly, we can bring more types to the current query thingy. Strings that are being parsed later don't play well with TypeScript: we need more semantics than that. We'd have to make some sort of query builder, like the following.
const queryParams = queryBuilder.orderByChild('score').fluentApi('likeThis')
This would return some type such as Query<'score', 'likeThis'>
which could be further used to check if score
and likeThis
are valid keys from the schema.
What about the rest?
I'm still exploring Firebase so some of these ideas might be naive because only work in very basic cases, so please tell me downsides and cases that this wouldn't cover.
Of course, this idea isn't only for Real Time Database (RTDB), but for all other similar things that Firebase has to offer.