Skip to content

web: constrain the generic types for props to objects #74

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions frontends/web/src/decorators/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import { RenderableProps } from 'preact';
import { ObjectButNotFunction } from '../utils/types';

/**
* Describes the path of an API endpoint (available with the GET method).
Expand All @@ -26,7 +27,7 @@ export type Endpoint = string;
*
* Example: `{ propertyName: 'path/to/endpoint' }`
*/
export type EndpointsObject<LoadedProps> = {
export type EndpointsObject<LoadedProps extends ObjectButNotFunction> = {
readonly [Key in keyof LoadedProps]: Endpoint;
};

Expand All @@ -35,4 +36,4 @@ export type EndpointsObject<LoadedProps> = {
*
* Example: `props => 'subject/' + props.id + '/attribute`
*/
export type EndpointsFunction<ProvidedProps, LoadedProps> = (props: RenderableProps<ProvidedProps>) => EndpointsObject<LoadedProps>;
export type EndpointsFunction<ProvidedProps extends ObjectButNotFunction, LoadedProps extends ObjectButNotFunction> = (props: RenderableProps<ProvidedProps>) => EndpointsObject<LoadedProps>;
6 changes: 3 additions & 3 deletions frontends/web/src/decorators/load.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { Component, ComponentFactory, h, RenderableProps } from 'preact';
import { getDisplayName } from '../utils/component';
import { apiGet } from '../utils/request';
import { KeysOf } from '../utils/types';
import { KeysOf, ObjectButNotFunction } from '../utils/types';
import { Endpoint, EndpointsFunction, EndpointsObject } from './endpoint';

// Stores whether to log the time needed for individual API calls.
Expand All @@ -33,7 +33,7 @@ let logCounter = 0;
* @param renderOnlyOnceLoaded - Whether the decorated component shall only be rendered once all endpoints are loaded.
* @return A function that returns the higher-order component that loads the endpoints into the props of the decorated component.
*/
export function load<LoadedProps, ProvidedProps = {}>(
export function load<LoadedProps extends ObjectButNotFunction, ProvidedProps extends ObjectButNotFunction = {}>(
endpointsObjectOrFunction: EndpointsObject<LoadedProps> | EndpointsFunction<ProvidedProps, LoadedProps>,
renderOnlyOnceLoaded: boolean = true, // Use false only if all loaded props are optional!
) {
Expand Down Expand Up @@ -67,7 +67,7 @@ export function load<LoadedProps, ProvidedProps = {}>(
const newEndpoints = this.determineEndpoints();
// Load the endpoints that were different or undefined before.
for (const key of Object.keys(newEndpoints) as KeysOf<LoadedProps>) {
if (oldEndpoints == null || newEndpoints[key] !== oldEndpoints[key]) {
if (oldEndpoints === undefined || newEndpoints[key] !== oldEndpoints[key]) {
this.loadEndpoint(key, newEndpoints[key]);
}
}
Expand Down
3 changes: 2 additions & 1 deletion frontends/web/src/decorators/share.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { Component, ComponentFactory, h, RenderableProps } from 'preact';
import { getDisplayName } from '../utils/component';
import { ObjectButNotFunction } from '../utils/types';
import { Store } from './store';

/**
Expand Down Expand Up @@ -47,7 +48,7 @@ import { Store } from './store';
* export { HOC as Counter };
* ```
*/
export function share<SharedProps, ProvidedProps = {}>(
export function share<SharedProps extends ObjectButNotFunction, ProvidedProps extends ObjectButNotFunction = {}>(
store: Store<SharedProps>,
) {
return function decorator(
Expand Down
3 changes: 2 additions & 1 deletion frontends/web/src/decorators/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
*/

import { Component } from 'preact';
import { ObjectButNotFunction } from '../utils/types';

/**
* This class allows all instances of a component to share a common state.
*/
export class Store<State> {
export class Store<State extends ObjectButNotFunction> {
private components: Component[] = [];

/**
Expand Down
4 changes: 2 additions & 2 deletions frontends/web/src/decorators/subscribe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { getDisplayName } from '../utils/component';
import { equal } from '../utils/equal';
import { apiSubscribe, Event } from '../utils/event';
import { apiGet } from '../utils/request';
import { KeysOf } from '../utils/types';
import { KeysOf, ObjectButNotFunction } from '../utils/types';
import { Endpoint, EndpointsFunction, EndpointsObject } from './endpoint';
import { load } from './load';

Expand All @@ -31,7 +31,7 @@ import { load } from './load';
* @param subscribeWithoutLoading - Whether the endpoints shall only be subscribed without loading them first.
* @return A function that returns the higher-order component that loads and updates the endpoints into the props of the decorated component.
*/
export function subscribe<LoadedProps extends object, ProvidedProps = {}>(
export function subscribe<LoadedProps extends ObjectButNotFunction, ProvidedProps extends ObjectButNotFunction = {}>(
endpointsObjectOrFunction: EndpointsObject<LoadedProps> | EndpointsFunction<ProvidedProps, LoadedProps>,
renderOnlyOnceLoaded: boolean = true, // Use false only if all loaded props are optional!
subscribeWithoutLoading: boolean = false,
Expand Down
7 changes: 4 additions & 3 deletions frontends/web/src/decorators/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { ComponentFactory } from 'preact';
// @ts-ignore ('frontends/web/node_modules/react-i18next/dist/commonjs/index.js' implicitly has an 'any' type)
import { translate as originalTranslate } from 'react-i18next';
import { ObjectButNotFunction } from '../utils/types';

// The types in 'https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-i18next'
// use the types of React instead of Preact, thus we define our own types in this file.
Expand Down Expand Up @@ -55,14 +56,14 @@ export interface TranslateProp {

type Namespaces = string | string[];

type NamespacesFunction<ProvidedProps> = (props: ProvidedProps) => Namespaces;
type NamespacesFunction<ProvidedProps extends ObjectButNotFunction> = (props: ProvidedProps) => Namespaces;

type NamespaceOptions<ProvidedProps> = Namespaces | NamespacesFunction<ProvidedProps>;
type NamespaceOptions<ProvidedProps extends ObjectButNotFunction> = Namespaces | NamespacesFunction<ProvidedProps>;

/**
* This function provides type safety for the 'react-i18next' translate decorator.
*/
export function translate<ProvidedProps = {}>(
export function translate<ProvidedProps extends ObjectButNotFunction = {}>(
options?: NamespaceOptions<ProvidedProps>,
) {
return function decorator(
Expand Down
8 changes: 8 additions & 0 deletions frontends/web/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@
* Array with keys of the given type.
*/
export type KeysOf<T> = Array<keyof T>;

/**
* Constrain a type to objects without allowing functions.
* (See https://github.com/Microsoft/TypeScript/issues/27278.)
* As it turns out, you need TypeScript 3 to enforce this constraint.
* At the moment, we are using version 2.9.2 (yarn run tsc -version).
*/
export type ObjectButNotFunction = object & { prototype?: never; };