Skip to content
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

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.

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

No branches or pull requests

7 participants