Skip to content

Suggestion for optional parameter with type 'never' #14452

@ikatyang

Description

@ikatyang

The type never currently just mean the function return that will never arrive or the unexpected value after type inference.

I think the type never can be more powerful.

optional parameter: using type never

Since undefined has two meaning: not existed and existed but no value, it would be better to split it out to have the clearest meaning.

  • It currently uses the ? operator to define a optional parameter:

    interface IExample {
      a?: string;
    }
    const test1: IExample = {
      a: undefined, // expected error, but passed
    };
    
    function funcExample(a?: string) {}
    funcExample(undefined); // expected error, but passed

    It is expected to be error, but it is passed because it was resolved as the following code in strictNullChecks mode:

    interface IExample {
      a?: string | undefined
    }
    function funcExample(a?: string | undefined): void

    where the undefined should not appear since it just can be string or void, so I think it should be resolved as:

    interface Example {
      a: string | never
    }
    function funcExample(a: string | never) {}

    and expect its behavior as:

    const example1: Example = {
      a: undefined; // show error message
    };
    const example2: Example = {}; // expected passed, but error now
    
    funcExample(undefined); // show error message
  • For more about never with function optional parameter:

    function neverExample(a: never) {}
    neverExample(); // expected passed, but error now

    Why dont I just use the following code?

    function neverExample() {}

    Consider the following generic class:

    abstract class Matcher<T> {
      public abstract rawMatch(target: T): boolean;
      public match(target: T): boolean {
        return this.rawMatch(target);
      }
    }
    class TestMatcher extends Matcher<never> {
      constructor(public bool: boolean) {
        super();
      }
      public rawMatch(): boolean {
        return this.bool;
      }
    }
    const result = new TestMatcher(true).match(); // expected passed, but error now

    It will be more convenient if we can use never with function in generic class.

    The only way to solve this problem now is to override the method:

    class TestMatcher extends Matcher<null> {
      constructor(public bool: boolean) {
        super();
      }
      public rawMatch(): boolean {
        return this.bool;
      }
      public match(): boolean {
        return super.rawMatch(null);
      }
    }

alias for { [key: string]: T; }

{ [key: string]: T } is a type that we use it frequently, but it looks not comfortable like number, object, etc.
I'd like to suggest to have an alias for it globally, for example:

type dictionary<T> = { [key: string]: T; };
function setData(data: dictionary<any>) { ... }
function setData(data: { [key: string]: any }) { ... }
function setData(data1: dictionary<any>, data2: dictionary<any>, something: any) { ... }
function setData(data1: { [key: string]: any }, data2: { [key: string]: any }, something: any) { ... }

disabling rules with inline comments

I don't know if this is duplicate ( issue 11051 ) or not, since the following code is 100% error in type-checking but 100% correct in logic.

Type inference is powerful, but sometimes it'll block some convenient usage.
Given the following class, the generic is consider to be a state of it:

type MapCallback<Input, Output> = (value: Input) => Output;

abstract class Querier<Target, Result> {

  public mapCallback: MapCallback<any, any>;

  public abstract rawQuery(target: Target): Result[];
  public query(target: Target): Result[] {
    const results = this.rawQuery(target);
    return results.map(this.mapCallback);
  }

  public map<MappedResult>(callback: MapCallback<Result, MappedResult>): Querier<Target, MappedResult> {
    this.mapCallback = callback;
    return <Querier<Target, MappedResult>>this; // error: cannot be converted
  }

}

The map method will change its state, for example:

class SomeQuerier extends Querier<object, number> { ... }
const someResults = 
  new SomeQuerier() // Querier<object, number>
    .map((num: number) => num.toString()) // Querier<object, string>
    .query(someObject); // string[]

So I think is there a way to provide some inline comments to disable rules, just like eslint and tslint did?

public map<MappedResult>(callback: MapCallback<Result, MappedResult>): Querier<Target, MappedResult> {
    this.mapCallback = callback;
    // tsc:disable-next-line type-convert-checking
    return <Querier<Target, MappedResult>>this;
  }

Edit: I found the following part is duplicate ( issue 10727 ), just ignore it.

Relative complement operator

We have union operator ( | ) now, from myType1 to myType2:

type myType1 = string | number;
type myType2 = myType1 | boolean;
// myType2 = string | number | boolean

But we can't do the opposite way from myType2 to myType1:

type myType2 = string | number | boolean;
type myType1 = myType2 - boolean; // we cannot do this now
// expected myType1 = string | number;

If we can have the complement operator ( - ), it will be easily to use in some Partial case (e.g. options ):

interface PartialOptions {
  a?: string;
  b?: number;
  c?: {
    c1: string;
    c2: number;
  }
}

If we want to get types within c, it is impossible to do this now since c can be undefined:

type cnames = keyof PartialOptions['c'];
// cnames = never, since PartialOptions['c'] could be undefined
type ctypes = PartialOptions['c'][cnames]; // error: Type 'never' cannot be used as an index type

The only way to get types now is to copy it manually. but we have to copy it again while PartialOptions changed:

type ctypes = string | number;

If we can use complement operator:

type EntireOptions = {
  [T in keyof PartialOptions]: PartialOptions[T] - undefined;
}
// expected EntireExmaple = {
//   a: string;
//   b: number;
//   c: {
//     c1: string;
//     c2: number;
//   }
// }
type cnames = keyof EntireOptions['c'];
// expected cnames = 'c1' | 'c2'
type ctypes = EntireOptions['c'][cnames];
// expected ctypes = string | number

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions