2

I am in this scenario where I don't know what a type will look like, whether it is undefined or a boolean, string or a number, etc. I am working on a library that will parse some string content and return an object of properties that the user has described using "type objects".

The Type is an interface for how the type objects should look like, and it goes like this:

/**
 * A type object.
 */
export default interface IType<O>
{
    /** The name of the type. */
    name: string;

    /** A description of the type. */
    description?: string;

    /**
     * Parse the given input content and return the parsed output value.
     * @param input The given input value.
     * @returns The parsed output.
     */
    parse(input: string): O;
}

I have provided the user with 3 pre-defined "type objects" that follow this interface (Number as numberType, Boolean as booleanType and String as stringType). The user can use these types to gather arguments from a string.

The argument interface defines how an argument should be specified in code.

/**
 * An object describing how an argument will look like.
 */
export default interface IArg<T>
{
    /**
     * The name of the argument, this is how you use the argument later
     * in the command object.
     */
    name: string;

    /** A description about the argument. */
    description?: string;

    /** The type the argument should be resolved with. */
    type: IType<T>;

    /** Should this take all the parameters left of the string? */
    rest?: boolean;

    /** Is the argument required? */
    required?: boolean;
}

The user creates a command class and passes some arguments to the constructor including an array of arguments;

[
    {
        name: "age"
        type: numberType
    },
    {
        name: "name",
        type: stringType
    }
]

When the command string is parsed these arguments will be added into an object input.args. Somehow I want the typescript parser to know what may or may not be provided by the information given by the user.

E.g. if an argument is required and is a type of IArg<string> (let's call it name) then the parser knows that input.args.name is definetly a string, but if it isn't required, it might be undefined or a string.

Is this possible to achieve?

Edit: I found this answer by jcalz which takes an object of key-value pairs which does exactly what I'm looking for; but I'm unsure on how to implement it with arrays.

2 Answers 2

0

Here is what I came up with:

First remove the required parameter from your interface definition. The reason for this will become evident soon:

interface IArg<T> {
    /**
     * The name of the argument, this is how you use the argument later
     * in the command object.
     */
    name: string;

    /** A description about the argument. */
    description?: string;

    /** The type the argument should be resolved with. */
    argType: IType<T>;

    /** Should this take all the parameters left of the string? */
    rest?: boolean;
};

Now I use this contrived example of a token parser which is supposed to parse a token only if it is required or is available (i.e. not null).

// Used to check if the IArg has a required parameter that is set to true
function argIsRequired(arg: IArg<unknown> & {required?: boolean}): boolean {
    return arg.required === true;
}

function parseToken<T>(token: string, decodeToken: IArg<T> & { required: true }): T;
function parseToken<T>(token: string, decodeToken: IArg<T>): T | undefined;
function parseToken<T>(token: string | null, decodeToken: IArg<T>) {
    if (token == null) {
        if (argIsRequired(decodeToken)) {
            throw Error("required token not found");
        } else {
            return undefined;
        }
    }
    return decodeToken.argType.parse(token);
}

The purpose for the overloads of the function is to help the type checker decide whether or not to allow certain return types (T vs T | undefined).

By removing the required parameter from the IArg interface, we can now specialize it in the parser function to act as a type guard for the function.

And now to test, we define two possible args where one is required and the other isn't:

const ageArg = new class implements IArg<number> {
    readonly name = "age"
    readonly argType = numberType;
    readonly required = true;
};

const nameArg = new class implements IArg<string> {
    readonly name = "name"
    readonly argType = stringType;
};

If the above definition is correct, the type checker should give us an error if we try to use the wrong type for the values returned by the parseToken function above.

const age: number = parseToken('24', ageArg); // This is Ok
// const name: string = parseToken("", nameArg); // This gives error
const name: string | undefined = parseToken("", nameArg); // This is Ok

Playground link

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, although parsing the arguments is not really a problem, the problem is how to let the typescript parser know what properties are available in the input.args object.
0

After a lot of tinkering around I ended up never finding out how to do it. Some genius at "The Programmer's Hangouts" discord server spent around 10 minutes and solved the issue with the help of the SO link I added to my post and some of his billiant thinking!

https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=33&pc=1#code/C4TwDgpgBAggTgcygXigbynCBDAJgewDsAbEKQ7AWwgC4oBnYOAS0IQBpMcCSzRI6jFm05Y8RUlwCOAV2ZZcAfjoAjfPmI5CUAL4BuAFAGAxkUZRKIeAnoooAbQNRn6clVpQARADN5jAHLunpz8Hp5CrAjB0nIKdEwy0DrsTi4YFNR0ntgIENGhWYQylCoQcJ66BgC6UNi2poSMhgaswGXe2MbQACrgEACy2GDoqUUlZXRjpXCGzmoaWqrqmtiEs1AyhLgQvoQQuHSb27v76xFsgkyR6-gqAFYQxsB0tw9PUAA+5DLExIY6RlCUAA8vdHsAAGJwfCUAAiEHoxhYYGA+DgAB5ulAIAAPNpbWzWAB8qWcqG69k8GTyNVx+NwtnOCFJUEUUApnlCnlpeIgBKgAGsICB8N52X1BmAWWyOVhZPJ9tzsbz+QkINLXPYANJQVjsynU7lVOi9SCS+wcrlVGoAlxQOgYbW67Qcw3W5Tis1DC2Uq02lmTCAANzKAfIwbKzSBsOYERUMja6IAqsr6YTEES7Cm6XyGbBEKyQWCnlCYfDEcjURik5nAyGZoC+lAk4RmERuvgAJKENpwejgtuEZOZ1CpAAU2ZVedWZDZY4FdCTAEoUJmg-hmLh7eH6yuc-yx-O6KxvGUoJ2V8g1xvcCu2Z3t3t643IFAABpZ1vtrs9sr9p6DuiMZxgmEDomOoSihYViIPQS72FMZRVESRLNJowBQDidAfqgGC+H2wCBJkXgAFL4AAFoQ0Q5B4ABMACsuiGEAA

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.