Skip to content

Use the default value when asserting the type of a parameter #60054

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

Closed
6 tasks done
denis-migdal opened this issue Sep 25, 2024 · 8 comments
Closed
6 tasks done

Use the default value when asserting the type of a parameter #60054

denis-migdal opened this issue Sep 25, 2024 · 8 comments
Labels
Duplicate An existing issue was already created

Comments

@denis-migdal
Copy link

πŸ” Search Terms

use default value to assert generic type
default value parameters destructuring

βœ… Viability Checklist

⭐ Suggestion

It is common to give default parameters to functions :

function foo<T = null>(a: T = null) { return a; }

The issue is that this causes the following error :

Type 'null' is not assignable to type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to 'null'.

Requiring us to do a quite dirty cast : null as unknown as T.

It could be nice if TS made use of defaults parameters values (and defaults destructured parameters values) when asserting the generics types. And only raise an error when calling the function with a generic type incompatible with the default value:

function foo<T>(a: T = null) { return a; } // currently unknown, should be null.

foo<number>(); // would raise an error "generic type T=number incompatible with type of "a" default value.

I assume the issue comes from .d.ts files where we can't write default values as they are implementation details, therefore having to write:

function foo<T>(a?: T);
// or
function foo<T = number>(a?: T);

Maybe we could introduce a new notation to explicit the type of the default values for .d.ts files ?

function foo<T>(a?number: T); // if a not given, then a (and T) are "number".

πŸ“ƒ Motivating Example

This could be quite useful when destructuring parameters:

type Opts<T, U, V, ...> = {
    a: T,
    b: U,
    c: V
    ...
}
function foo<T, U, V, ...>({a = 43, c = 42, b = 43}: Opts<T,U,V,...> = {});
// with no needs for Partial<> when default values are provided, to ensure correct generic parameters assertions.

function foo<O extends Opts<...>>({a = 43, c = 42, b = 43}: O = {});

πŸ’» Use Cases

Cf explanations in suggestion.

@MartinJohns
Copy link
Contributor

Requiring us to do a quite dirty cast : null as unknown as T.

No, it just requires you to be accurate with your types: a: T | null = null

If you write function foo<T = null>(a: T = null) { return a; }, then this is a perfectly legal invocation: foo<number>() - the return type of that function is inferred based on the argument a, which is typed T and in the example invocation being number, but the actual returned value is null.

@denis-migdal
Copy link
Author

denis-migdal commented Sep 25, 2024

Requiring us to do a quite dirty cast : null as unknown as T.

No, it just requires you to be accurate with your types: a: T | null = null

This requires to write 3 times the same information:

function foo<T = null>(a: T|null = null) { return ...; }

Also, I had 2 issues in other (more complex) contexts where :

  • <T = null> made TS "lazy" and always infer T as null (removing it made TS correctly infer types).
  • T|null prevented to correctly infer T when the value was null (the type T is then the default value of <T = null>).

When destructuring:

{
    type Opts<T> = { a: T|null };

    function foo<O extends Opts<T>, T = null>({a = null}: Partial<O> = {}) { return null as unknown as T }
    let a = foo({a: null}); // null
    let b = foo();          // null
    let c = foo({a: 4});    // null
}
{
    type Opts<T> = { a: T|null };

    function foo<O extends Opts<T>, T>({a = null}: Partial<O> = {}) { return null as unknown as T }
    let a = foo({a: null}); // unknown
    let b = foo();              // unknown
    let c = foo({a: 4});     // unknown
}
{
    type Opts<T> = { a: T|null };

    function foo<O extends Opts<unknown>>({a = null}: Partial<O> = {}) { return null as unknown as O["a"] }
    let a = foo({a: null}); // null
    let b = foo();              // unknown
    let c = foo({a: 4});     // number
}
// other tests in the playground

IIRC, doing function foo<T = null>({a} : Opts<T>){...} causes less issues than function foo<O extends Opts...>({a} : O<T>){...}, but they are not strictly equivalent.

Playground Link

If you write function foo<T = null>(a: T = null) { return a; }, then this is a perfectly legal invocation: foo<number>()

Which is an issue as a could then be of a type incompatible with T.

the return type of that function is inferred based on the argument a, which is typed T and in the example invocation being number, but the actual returned value is null.

I could use typeof a instead of T, but its type would then be null|T.
Note: typeof a is unknown if we don't set a default type for T.

I guess a complete workaround would be:

type X2<T> = Exclude<T, any> extends never ? null : null;
type X<A, T, D> = Exclude<A, D> extends never ? D : T;
// + need to handle A = any ?


function foo<T = null>(a: T|X2<T> = null) {
    return {} as unknown as X<typeof a, T, null>;
}

let a = foo();        // null
let b = foo(34);   // number
let c = foo(null); // null
let x = {} as unknown as number|null;
let d = foo(x); // null|number

EDIT: this may also cause issues when doing extends ReturnType foo<infer T>.

Maybe I'm missing something simple and obvious.

I think that a notation to indicate the type of the default value would make things easier and more explicit.

@jcalz
Copy link
Contributor

jcalz commented Sep 25, 2024

Duplicate of or strongly related to #58977

@denis-migdal
Copy link
Author

denis-migdal commented Sep 25, 2024

Duplicate of or strongly related to #58977

Indeed, this is strongly related. My approach wasn't really to "defer" the type check, which may explain why I didn't found it when searching for existing issues.

My approach was to assume the type of the default value is the type of the generic type if no arguments are provided (and to raise an error if an incompatible generic type is explicitly provided). But, in practice, I guess this is almost the same thing.

From the issue you cited:

This is the sort of thing that really does need .d.ts parity to be useful, and the problem here is how to represent this in .d.ts files, where default values aren't manifested.

I think my feature request provides a possible representation for .d.ts files using types instead of values.

That said, people really want to write f(n = 0) in declaration files for documentation purposes, even if that value isn't "checked" per se

My feature request could allow to write it as f(n?0: number) (here, 0 is a type).
Well, not the prettiest thing.

For the :

declare const aa: true | undefined;
const j = x(aa);

I think the behavior should depends on the TS flag (I don't remember its name) allowing or disallowing to pass undefined to a foo(a?: number).

If giving undefined is allowed, then T should be : Exclude<typeof aa, undefined> | the_default_value_type.

EDIT: This could behave a little like :

function foo<T extends null>(a?: T): T
function foo<T>(a: T): T
function foo(a: unknown = null) { return a; }

let a = foo();   // null
let b = foo(43); // number
let c = foo<number>(); // number does not satisfy the constraint 'null'

@MartinJohns
Copy link
Contributor

MartinJohns commented Sep 25, 2024

I think the behavior should depends on the TS flag (I don't remember its name) allowing or disallowing to pass undefined to a foo(a?: number).

There is no such flag, it's always allowed to pass undefined. You're likely confusing it with exactOptionalPropertyTypes, which is for properties. There's the open issue #44548 for this.

@denis-migdal
Copy link
Author

There is no such flag, it's always allowed to pass undefined. You're likely confusing it with exactOptionalPropertyTypes

Indeed. I misremembered an issue I had with :

function foo({a}: {a?: number} = {}) { return a; }
foo({a: undefined});

@denis-migdal
Copy link
Author

Maybe the foo<T>(a?null: T) and foo<T>(a: T = null) notations could be sugar syntax for something like :

function foo<T extends null>(a?: T) // or function foo<T extends typeof default_value>(a?: T)
function foo<T>(a: T)
function foo(a: unknown = null) { ... }

Which would help for parameters destructuring as we'd need 2^n signature calls ?

// both not given
function foo<T extends null, U extends null>({a, b}: {a?: T, b?: U})
// one given
function foo<T extends null, U>({a, b}: {a?: T, b: U})
function foo<T, U extends null>({a, b}: {a: T, b?: U})
// both given
function foo<T, U>({a: T, b: U})
function foo({a = null, b = null}: {a: unknown, b:unknown}) { ... }

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Sep 25, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Sep 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants