Skip to content
New issue

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

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

Already on GitHub? # to your account

how do i prevent non-nullable types in arguments #22375

Closed
zpdDG4gta8XKpMCd opened this issue Mar 7, 2018 · 9 comments
Closed

how do i prevent non-nullable types in arguments #22375

zpdDG4gta8XKpMCd opened this issue Mar 7, 2018 · 9 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@zpdDG4gta8XKpMCd
Copy link

so we have the following convenience function:

export function asDefinedOr<T, D>(value: T | undefined, defaultValue: D): T | D {
    return value !== undefined ? value : defaultValue;
}

we would like to prevent the following from happening

declare var x : number;
asDefinedOr(x, 0); // <-- problem, unnecessary call, because x cannot be undefined

so we would like to make only T | undefined types allowed as aguments but not just T

is there a way to do it using conditional types?

@jcalz
Copy link
Contributor

jcalz commented Mar 7, 2018

This question should probably be on Stack Overflow or Gitter instead of here, if I understand the rules correctly.

But I can't stop myself because I love this sort of thing. Using conditional types:

export function asDefinedOr<T extends (undefined extends T ? any : undefined), D>(
  value: T, defaultValue: D
): (T extends undefined ? never : T) |  D {
  return value !== undefined ? value : defaultValue;
}

declare var x : number;
const xD = asDefinedOr(x, 0); // error
// Argument of type number is not assignable to parameter of type 'undefined'.

declare var y : number | undefined;
const yD = asDefinedOr(y, 0); // okay, yD is number

declare var z: string | undefined;
const zD = asDefinedOr(z, 0); // okay, zD is string | 0

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Mar 7, 2018

i asked about the Not operator #22323 (comment)

which turned out being defined like this:

type Not<T, U> = T extends U ? never : T;

can my question be answered using it? something like:

export function asDefinedOr<T, D>(value: Not<T, NonNullable<T>>, defaultValue: D): T | D {

@ahejlsberg ahejlsberg added the Question An issue which isn't directly actionable in code label Mar 7, 2018
@jcalz
Copy link
Contributor

jcalz commented Mar 7, 2018

No, that doesn't work. What's wrong with the code I wrote above?

@zpdDG4gta8XKpMCd
Copy link
Author

i don't like any there

@jcalz
Copy link
Contributor

jcalz commented Mar 7, 2018

O....kay, then maybe

type top = {} | void | null;
export function asDefinedOr<T extends (undefined extends T ? top : undefined), D>(
  value: T, defaultValue: D
): (T extends undefined ? never : T) | D {
  return value !== undefined ? value as (T extends undefined ? never : T) : defaultValue; 
}

Note that T extends any and T extends top are not very different. Control flow analysis doesn't seem to know how to deal with conditional types the way I'd like, so I had to assert that value will be T extends undefined ? never : T in the "then" branch of the ternary expression for it to type check.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 7, 2018

+1 for @jcalz, not sure I would include the void part though.

@jack-williams
Copy link
Collaborator

jack-williams commented Mar 7, 2018

Interesting that you can constrain a type parameter based on itself! Cool solution @jcalz

@SalathielGenese
Copy link

SalathielGenese commented Mar 8, 2018

I just feel like another reason to implemented type exclusion...

Now, coming to @jcalz solution... I still don't understand how that constraint after function parameters work... It seem like the solution I've been looking for over two weeks now.

I purpose to sign a decorator @Inject with different signature depending whether it is applied on static or instance side :

  • Static side : (cfg?: {type: SymbolConstructor | ConstructorLike})
  • Instance side : (cfg?: {id: string} | {type: SymbolConstructor | ConstructorLike} | {id: string, type: SymbolConstructor | ConstructorLike})

The instance side allow specifying the id. Here is how far I have gone :

declare global
{
    interface ConstructorLike<T = {}>
    {
        new(...args: any[]): T;
    }
}
export const Inject: {
    (cfg?: {type: SymbolConstructor | ConstructorLike}): <C extends ConstructorLike<T>, K extends keyof C, T>(context: C, property: K, index?: number) => void;
    (cfg?: {id: string} | {type: SymbolConstructor | ConstructorLike} | {id: string, type: SymbolConstructor | ConstructorLike}): <C extends T, K extends keyof C, T>(context: C, property: K, index?: number) => void;
} = <any>void 0; //TODO: implements the injection logic

...and the use case bellow fail even where it is not expected :

class Clazz
{
    @Inject()
    public string!: string;

    @Inject({id: 'number'})
    public number!: number;

    @Inject({type: Array})
    public array!: any[];

    @Inject({id: 'symbol', type: Symbol})
    public symbol!: Symbol;

    public method(@Inject() arg0: string, @Inject({type: Symbol}) arg1: symbol, @Inject({id: 'number'}) arg2: number, @Inject({id: 'array', type: Array}) arg3: any[])
    {
    }



    @Inject()
    public static string: string;

    @Inject({id: 'number'})
    public static number: number;

    @Inject({type: Clazz})
    public static prop: any[];

    @Inject({id: 'symbol', type: Symbol})
    public static symbol: Symbol;

    public constructor(@Inject() arg0: string, @Inject({type: Symbol}) arg1: symbol, @Inject({id: 'number'}) arg2: number, @Inject({id: 'array', type: Array}) arg3: any[])
    {
    }

    public static method(@Inject() arg0: string, @Inject({type: Symbol}) arg1: symbol, @Inject({id: 'number'}) arg2: number, @Inject({id: 'array', type: Array}) arg3: any[])
    {
    }
}

Any help is welcome. Thanks.

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

7 participants