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

Proposal: Add type Optional<T> #20984

Closed
ricokahler opened this issue Jan 3, 2018 · 5 comments
Closed

Proposal: Add type Optional<T> #20984

ricokahler opened this issue Jan 3, 2018 · 5 comments

Comments

@ricokahler
Copy link

Context/use case

I think it would be really useful to have a library that allows javascript users to define interfaces without using typescript syntax. This is my attempt at it (inspired by React's PropTypes):

// define a function
const myFunction = defineFunction(
  // define the interface for the `props` of the function using mapped types
  propTypes => ({
    stringParam: propTypes.string,
    optionalNumberParam: propTypes.number.isOptional,
    objectParam: propTypes.shape(propTypes => ({
      nestedParam: propTypes.string,
    })),
    objectParamFromExample: propTypes.typeof({
      foo: '',
      bar: 0,
      someDate: new Date(),
    })
  }),
  // define the function to be returned. the type of `props`
  // is the result of the mapped type above
  props => {
    props.objectParam.nestedParam
    return props.stringParam;
  }
);

/**
 * use the function. The type inferred by typescript is:
 * 
 * ```ts
 * const myFunction: (prop: {
 *   stringParam: string;
 *   optionalNumberParam: number | undefined;
 *   objectParam: {
 *       nestedParam: string;
 *   };
 *   objectParamFromExample: {
 *       foo: string;
 *       bar: number;
 *       someDate: Date;
 *   };
 * }) => string
 * ```
 */
myFunction({
  stringParam: '',
  // no error now
  optionalNumberParam: undefined,
  objectParam: {
    nestedParam: '',
  },
  objectParamFromExample: {
    foo: '',
    bar: 0,
    someDate: new Date(),
  }
});

interface PropType<T> { isOptional: PropType<T | undefined> }
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
  string: PropType<string>,
  number: PropType<number>,
  boolean: PropType<boolean>,
  // ...
  // object, array, symbol, etc...
  // ...
  shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>,
  typeof: <R>(typeToQuery: R) => PropType<R>
}

/**
 * the only purpose of this function is to capture the type and map it appropriately.
 * it doesn't do anything else
 */
function defineFunction<Props, R>(
  props: (types: PropTypes) => V<Props>,
  func: (prop: Props) => R
) {
  return func;
}

This is a really motivating use case for me because I could create factory functions that create React stateless components or classes that add PropTypes and also capture the type the PropTypes define. Javascript users could use typescript without using typescript (but this proposal goes beyond my motivation).

The issue

But here's the issue:

image

There no way (that I know of) to map a particular property to be optional. I understand the type Partial<T> exists but this asserts that all of the properties are optional instead of just one.

Even if the type of optionalNumberParam is number | undefined, the typescript compiler still requires me to put optionalNumberParam: undefined in the object literal of the prop for the error to go away.

image

I opened a stackoverflow question about this topic here.

Proposal

A possible solution to this problem would be to introduce a special type type Optional<T>.
The type would be equal to this:

type Optional<T> = T | undefined;

and additionally, the Optional<T> type would assert that on whichever parent the type is used on, it will be optional.

So the interface PropType would go from:

interface PropType<T> { isOptional: PropType<T | undefined> }

to:

interface PropType<T> { isOptional: PropType<Optional<T>> }

Code examples

Simple example

These interfaces would be synonymous (though, I prefer the first syntax):

interface A {
  foo: string,
  bar: number,
  optionalDate?: Date, // type: Date | undefined
}
interface A {
  foo: string,
  bar: number,
  optionalDate: Optional<Date>, // type: Date | undefined
}

Primary/complex example again

interface PropType<T> { isOptional: PropType<Optional<T>> }
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
  string: PropType<string>,
  number: PropType<number>,
  boolean: PropType<boolean>,
  // ...
  // object, array, symbol, etc...
  // ...
  shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>,
  typeof: <R>(typeToQuery: R) => PropType<R>
}

// define a function
const myFunction = defineFunction(
  // define the interface for the `props` of the function using mapped types
  propTypes => ({
    stringParam: propTypes.string,
    optionalNumberParam: propTypes.number.isOptional,
    objectParam: propTypes.shape(propTypes => ({
      nestedParam: propTypes.string,
    })),
    objectParamFromExample: propTypes.typeof({
      foo: '',
      bar: 0,
      someDate: new Date(),
    })
  }),
  // define the function to be returned. the type of `props`
  // is the result of the mapped type above
  props => {
    props.objectParam.nestedParam
    return props.stringParam;
  }
);

// use the function
myFunction({
  stringParam: '',
  // optionalNumberParam: undefined, // should be fine to omit this arg
  objectParam: {
    nestedParam: '',
  },
  objectParamFromExample: {
    foo: '',
    bar: 0,
    someDate: new Date(),
  }
});
@jcalz
Copy link
Contributor

jcalz commented Jan 3, 2018

This is related to #13195, I think. My offhand suggestion there was to use void to mean "missing key".

EDIT: I feel that #13195 is the underlying issue: TypeScript doesn't really let you consistently distinguish a missing property key from an undefined property value. They are similar enough in practice that it usually doesn't matter, but different enough to be annoying (e.g., TypeScript will yell at you if you leave out a property whose value can be undefined.)

@ricokahler
Copy link
Author

ricokahler commented Jan 3, 2018

@jcalz that does seems related but the issue I want to address is that there is no way to map a particular property to be optional when using mapped types--it's all or nothing with the type Partial<T> = { [P in keyof T]?: T[P] }.

Your suggestion would solve this issue though. If ? was equivalent to T | void, then the type of Optional<T> would be type Optional<T> = T | void and thus definable by the user.

@Jessidhia
Copy link

Jessidhia commented Jan 4, 2018

Would something like FancyPartial<T, U extends keyof T> = { [P in keyof T]: T[P] } & { [K in U]?: T[K] } work?

@jcalz
Copy link
Contributor

jcalz commented Jan 4, 2018

That works if you know what U should be. But since you can't (afaik) programmatically extract the keys of a type whose properties satisfy a type predicate (without something like #12424), there's no obvious way to use FancyPartial<> to solve the above problem.

@ricokahler
Copy link
Author

I think conditional types fixes this

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants