So below is my example testing code to implement into the wider solution. I've created an object of "services", each property is the service name with the value being the service object. Each service object holds a property of "commands" with the value being an object of commands (classe's).
These commands are executed via the CLI which always returns a string which is then split. index 0 of the array would be the service name, whilst index 1 would be the name of the command.
// Example Classes
class A {
init() {
console.log('I Am A');
}
}
class B {
init() {
console.log('I Am B')
}
}
// Example Services Structure
export const services = {
service1: {
commands: {
a: A
}
},
service2: {
commands: {
b: B
}
}
}
// Type Declarations
export type Services = typeof services;
export type ServicesKeys = keyof Services;
// Testing
const input = (prompt('Choose a Service') || '').split(' ');
if (input.length !== 2) Deno.exit();
if (input[0] in services) {
const sKey = input[0] as ServicesKeys;
const service = services[sKey];
const commands = service.commands;
if (input[1] in commands) {
// Run into issues here as `keyof typeof commands` is `never`
new commands[input[1] as keyof typeof commands]();
}
}
Everything essentially works fine until the new commands[input[1] as keyof typeof commands](); as the type of keyof typeof commands is set to never. Which I understand as commands can't have a AND b so keyof has to be never but how do I work with this?
You just need to define the type for your services object, like in the refactor below. If you want to restrict the keys for any part of the structure, you can simply replace string with your union/enum/etc.
Note: I supplied a substitute for the Deno namespace APIs used in your code so that you can run the playground example directly in your browser.
TS Playground link
const Deno = {
exit (code: number = 0): never {
throw new Error(`Exited with code ${code}`);
}
};
// Example Classes
type Command = {
init (): void;
};
class A implements Command {
init () {
console.log('I Am A');
}
}
class B implements Command {
init () {
console.log('I Am B')
}
}
// Type Declarations
type Service = {
commands: {
[commandName: string]: new () => Command;
};
};
type Services = { [serviceName: string]: Service };
// Example Services Structure
const services: Services = {
service1: {
commands: {
a: A
}
},
service2: {
commands: {
b: B
}
}
}
// Testing
const input = (prompt('Choose a Service') || '').split(' ');
if (input.length !== 2) Deno.exit();
const [serviceName, commandName] = input;
let command: Command | undefined;
if (serviceName in services) {
const {commands} = services[serviceName];
if (commandName in commands) {
command = new commands[commandName]();
}
}
command ? command.init() : console.log('No match');
Related
In the 3rd party lib, There's the following:
export interface Event extends Log {
args?: Result;
}
export interface TypedEvent<EventArgs extends Result> extends Event {
args: EventArgs;
}
export type InstallationPreparedEvent = TypedEvent<
[
string,
string
] & {
sender: string;
permissions : string;
}
>;
Then, in my code, I got this.
function prepareInstall(...): Promise<InstallationPreparedEvent> {
const event = ...;
return event;
}
Then, I got these choices, but I like none of them. The reason is, I simplified it, but it actualy contains 8 args, so code becomes ugly.
// choice 1.
function run() {
const event = await prepareInstall();
string sender = event.args.sender;
}
// choice 2
function run() {
const { event : {sender } } = await prepareInstall();
}
// choice 3
function run() {
const { sender } = (await prepareInstall()).args;
}
What I want to achieve is from prepareInstall, I want to return event.args directly so I'd use it like this:
const { sender } = await prepareInstall();
But returning event.args doesn't solve it as typescript in the run still requires me to do: event.args.sender. The important thing is I need returned type in prepareInstall to be InstallationPreparedEvent since in run, I want typescript to tell me what values exist(sender, permissions).
Hope I made sense.
If I understand correctly, you want to return only the arg prop from InstallationPreparedEvent, but with correct typing (right?).
You can specify a prop's type through bracket notation:
function prepareInstall(...): Promise<InstallationPreparedEvent["args"]> {
const event = await ...;
return event.args;
}
With this, you will get type hinting:
function run() {
const eventArgs = await prepareInstall();
const {sender} = eventArgs; // no complaints
const foo = eventArgs.foo; //Error: Property 'foo' does not exist on type '[string, string] & { sender: string; permissions: string; }'
}
I am trying to use lit-translate to translate my "Elmish" typescript website into different languages. I use webpack and dotnet.
Inside my index.ts I register the translate config:
registerTranslateConfig({
lookup: (key, config) => config.strings != null ? config.strings[key] : key,
empty: key => key,
loader: lang => {
return new Promise((resolve, reject) => {
resolve({"title": "Der Titel"});
})}
});
use("en");
(The loader is hardcoded because getting the localization file also didn't work, but that's not important for now).
Inside html I use get("title")or translate("title") to get the translation.
Instead of the translation, I either read [title] or
(part) => { partCache.set(part, cb); updatePart(part, cb); }
If I assign the result of translate() to a variable, I get the following result inside the Object:
TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
at Function.r (<anonymous>:1:83)
at Module../src/index.ts (http://localhost:8080/app.bundle.js:9800:10)
at __webpack_require__ (http://localhost:8080/app.bundle.js:12476:33)
at http://localhost:8080/app.bundle.js:13494:11
at http://localhost:8080/app.bundle.js:13497:12
I already tried disabling webpack strict mode.
The full class looks like the following:
export class App extends Application<Model, Message, {}> {
#property({type: Boolean})
hasLoadedStrings = false;
init(): [Model, Cmd] {
const initModel: Model = {
openPage: [{title: "Home"}, home]
}
return [initModel, Cmd.none]
}
constructor() {
super();
this.hasLoadedStrings = false;
}
shouldUpdate (changedProperties: Map<string | number | symbol, unknown>) {
return this.hasLoadedStrings && super.shouldUpdate(changedProperties);
}
async connectedCallback () {
super.connectedCallback();
await use("en");
this.hasLoadedStrings = true;
}
}
customElements.define('my-element', App);
I solved the problem by writing my own little translation library.
The lit-translate package contains errors if you use it as suggested in the documentation so feel free to use my solution:
translate.ts:
const de_config =
{
"Category": {
"Home": "Start",
},
"Home": {
"Welcome back,": "Willkommen zurück,"
};
export function translate(key: string) {
const keyParts = key.split(".");
const keyCategory = keyParts.shift();
const keyWord = keyParts.join('.');
let translationIndexes = typedKeys(de_config);
for(let translationIndex of translationIndexes)
{
if(translationIndex == keyCategory) {
let translationKeys = typedKeys(de_config[translationIndex]);
for(let translationKey of translationKeys) {
if(translationKey == keyWord) {
return de_config[translationIndex][translationKey];
}
}
}
}
return key;
}
function typedKeys<T>(o: T): (keyof T)[] {
return Object.keys(o) as (keyof T)[];
}
Access translations with:
import { translate } from './translate';
translate("Category.Home");
One could also store translation object in a different file, write a function to change language dynamically, etc...
enum Channel {
GITHUG = 'github',
GITLAB = 'gitlab',
}
interface Github {}
interface Gitlab {}
function foo (a, b) {}
The first parameter a is an enum (Channel) value
If parameter a is github, then the type of b is GitHub
If parameter a is gitlab, then the type of b is Gitlab
How to define the type of function foo?
Here you have some alternative ways:
enum Channel {
GITHUB = 'github',
GITLAB = 'gitlab',
}
interface Github {
type: 'Github'
}
interface Gitlab {
type: 'Gitlab'
}
/**
* First approach
*/
type Params = [Channel.GITHUB, Github] | [Channel.GITLAB, Gitlab]
function foo(...args: Params) {
if (args[0] === Channel.GITHUB) {
const x = args[1] // Github
}
}
type Overloading =
& ((a: Channel.GITHUB, b: Github) => void)
& ((a: Channel.GITLAB, b: Gitlab) => void)
/**
* Second approach
*/
const foo2: Overloading = (a: Channel, b: Github | Gitlab) => null as any
const result = foo2(Channel.GITHUB, { type: 'Github' }) // ok
/**
* Third approach
*/
function foo3(a: Channel.GITLAB, b: Gitlab): void
function foo3(a: Channel.GITHUB, b: Github): void
function foo3(a: Channel, b: Github | Gitlab) {
}
foo3(Channel.GITLAB, { type: 'Gitlab' }) // ok
Playground
Use conditional types technique. Look at example below or go to online playground in order to test in action (thanks for help to Joonas)
enum Channel {
GITHUG = 'github',
GITLAB = 'gitlab',
}
interface Github {
_foo: number
}
interface Gitlab {
_bar: number
}
type ParamType<C> = C extends Channel.GITHUG ? Github : Gitlab;
function foo<C extends Channel>(a: C, b: ParamType<C>): void {}
foo(Channel.GITLAB, { _foo: 1 }); // error
foo(Channel.GITLAB, { _bar: 1 }); // success
Please, let me know if it works or not )
Explain look at the code comments:
enum Channel {
GITHUG = 'github',
GITLAB = 'gitlab',
}
interface Github {}
interface Gitlab {}
// Map enum value and type
interface Foo {
github: Github
gitlab: Gitlab
}
// `${Channel}` => 'github' | 'gitlab'
// Get the type K of a through the passed parameter
// if K is `github`, Foo[K] is Github
// if k is `gitlab`, Foo[K] is Gitlab
function foo<K extends `${Channel}`>(a: K, b: Foo[K]) {}
Playground
I had a very similar problem, in my case, the second parameter type comes from an interface:
interface ArgTypes { [id: string]: any }
interface ChannelArgs extends ArgTypes{
github: {
hubhub: number
},
gitlab: {
lablab: number
},
...
}
type Arg<Args extends ArgTypes, Id extends keyof Args> = Args[Id]
My foo function has to work with all types implementing the interface, so it is generic itself:
foo<ChannelArgs>('github', {hubhub: 12})
Turns out, declaration of a generic type and an inferred type does not mix, but it is possible to put them in different brackets:
type Foo<Args extends ArgTypes> = <Id extends keyof Args>(
id: Id,
arg: Arg<Args, Id>
) => void
const gitFoo: Foo<ChannelArgs> = function(a,b): void {}
gitFoo('github', {hubhub: 12}) // no error
gitFoo('github', {lablab: 12}) // error
I have the following javascript function that returns a function containing additional methods given in the object arguments.
I think the code will be more understandable than an explanation:
var scopeFunction = (object) => {
// create main function if it doesn't exist
if (!object.main) object.main = function(){
alert("No main function for this scopeFunction");
};
// add all object keys to the main function that will be return
_.each(object, function(d,i){ object.main[i] = d; });
// return main function
return object.main;
};
I want to define properly this code in typescript, this is what I did, but atom typescript (that's where I've tested it) throws errors when I try to access the keys of my returned function object.
This is how my current code look:
// TYPES
namespace ScopeFunction {
export interface Function<Obj extends MakerObject, Key extends keyof Obj> {
(): Obj["main"];
[key: Key]: Obj[Key];
}
export interface MainFunction {
(...args:any[]) : any;
}
export interface MakerObject {
[key: string]: any;
main?: MainFunction;
}
export type Maker = <Obj extends MakerObject, Key extends keyof Obj>(object:Obj) => ScopeFunction.Function<Obj, Key>;
}
// FUNC
var scopeFunction:ScopeFunction.Maker = (object) => {
// create main function if it doesn't exist
if (!object.main) object.main = function(){
alert("No main function for this scopeFunction");
};
// add all object keys to the main function that will be return
_.each(object, function(d,i){ object.main[i] = d; });
// return main function
return object.main;
};
// TEST
var test = scopeFunction({
a: 1,
b: "3",
main: () => { console.log("testLog"); return 0; }
})
var test1 = test(); // WORKS OK
var test2 = test.main(); // ALERT: Property 'main' doesn't exist on type 'Function<{ a: number; b: string; main: () => number }, "main" | "a" | "b">'
var test3 = test.a; // ALERT: Property 'a' doesn't exist on type 'Function<{ a: number; b: string; main: () => number }, "main" | "a" | "b">'
Any idea where the problem is in my definition?
There are several problems with your code:
The definitions don't compile [key: Key]: Obj[Key] is not valid, and indexer argument must be either number or string (those and only those types are valid). You need to use a mapped type instead.
(): Obj["main"] will not be a call signature of the same type as Obj["main"] is will be a function that returns whatever the property of main is.
The type of the main function is too generic, and it will not preserve any argument types.
A solution that does what you expect might be:
namespace ScopeFunction {
export type Function<Obj extends MakerObject<(...args: any[]) => any>> = Obj['main'] & {
[P in keyof Obj]: Obj[P];
}
export interface MakerObject<TMain extends (...args: any[]) => any> {
main?: TMain;
}
export type Maker = <TObj extends MakerObject<(...args: any[]) => any>>(object: TObj) => ScopeFunction.Function<TObj>;
}
// FUNC
var scopeFunction: ScopeFunction.Maker = (object) => {
// create main function if it doesn't exist
if (!object.main) object.main = function () {
alert("No main function for this scopeFunction");
};
// return main function
return Object.assign(object.main, object);
};
// TEST
var test = scopeFunction({
a: 1,
b: "3",
main: (param: number) => { console.log("testLog"); return param; }
})
var test1 = test(10);
var test2 = test.main(10);
var test3 = test.a;
I have created a polyfill for the JavaScript Array;
if (Array.prototype.remove !== 'function') {
Array.prototype.remove = function (value) {
var idx = this.indexOf(value);
if (idx !== -1) {
return this.splice(idx, 1);
}
return false;
};
}
Now I am upgrading the original JavaScript project to a TypeScript project and the tsc complains about the usage of the .remove method:
class Archive {
documents: DocInfo[] = []; // <-- Array of class DocInfo
addDocument(document: DocInfo) {
...
}
deleteDocument(document: DocInfo) {
this.documents.remove(document);
^^^^^^
tsc complains here: TS2339:Property 'remove' does not exist on type 'DocInfo[]'
}
}
How can I tell the tsc about this extension?
I tried creating a typings file, but without any success:
declare module 'Array' {
export function removeByAttr(propertyName: string, propertyValue: any);
}
Thanks
The typings should extend Array<T> interface:
interface Array<T> {
remove(item: T): boolean;
}
Extending the Array class with an interface it's simple, you could try something like this:
Playground
interface Array<T> {
remove(o: T): Array<T>;
}
Array.prototype.remove = function (o) {
var idx = this.indexOf(o);
if (idx !== -1) {
return this.splice(idx, 1);
}
return this;
}
class DocInfo {
name: string ;
constructor(name) {
this.name = name;
}
}
class Archive {
documents: DocInfo[] = [];
addDocument(document: DocInfo) {
this.documents.push(document);
}
deleteDocument(document: DocInfo) {
this.documents.remove(document);
}
printDocuments() {
this.documents.forEach((item: DocInfo) => {
console.log(item.name);
});
}
}
const a = new Archive();
const _1 = new DocInfo('1');
const _2 = new DocInfo('2');
a.addDocument(_1);
a.addDocument(_2);
a.printDocuments();
a.deleteDocument(_1);
console.log('*********************');
a.printDocuments();
console.log('*********************');
a.addDocument(_1);
a.deleteDocument(_2);
a.printDocuments();