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

Predefined conditional types #21847

Merged
merged 3 commits into from
Feb 9, 2018
Merged

Predefined conditional types #21847

merged 3 commits into from
Feb 9, 2018

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Feb 9, 2018

This PR adds several predefined conditional types to lib.d.ts:

  • Exclude<T, U> -- Exclude from T those types that are assignable to U.
  • Extract<T, U> -- Extract from T those types that are assignable to U.
  • NonNullable<T> -- Exclude null and undefined from T.
  • ReturnType<T> -- Obtain the return type of a function type.
  • InstanceType<T> -- Obtain the instance type of a constructor function type.

Some examples:

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error

The Exclude type is a proper implementation of the Diff type suggested here #12215 (comment). We've used the name Exclude to avoid breaking existing code that defines a Diff, plus we feel that name better conveys the semantics of the type. We did not include the Omit<T, K> type because it is trivially written as Pick<T, Exclude<keyof T, K>>.

When applying ReturnType<T> and InstanceType<T> to types with multiple call or construct signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types (this would require us to support typeof for arbitrary expressions, as suggested in #6606, or something similar).

Fixes #19569.

@ahejlsberg ahejlsberg merged commit f1d7afe into master Feb 9, 2018
@ahejlsberg ahejlsberg deleted the predefinedConditionalTypes branch February 9, 2018 23:40
@mhegazy mhegazy mentioned this pull request Feb 10, 2018
@KiaraGrouwstra
Copy link
Contributor

We did not include the Omit<T, K> type because it is trivially written as Record<T, Exclude<keyof T, K>>.

I think that should have been Pick rather than Record. :)
Moreover, thanks for all the new features!

@ahejlsberg
Copy link
Member Author

@tycho01 Right you are! Fixed.

@AriaMinaei
Copy link

I wonder if it is possible to detect plain objects using the Exclude type? Something like:

const plainObj = {foo: 'bar'}
const nonPlainObj = new Error()
IsPlainObject<typeof plainObj> // true
IsPlainObject<typeof nonPlainObj> // false
IsPlainObject<string> // false

@KiaraGrouwstra
Copy link
Contributor

KiaraGrouwstra commented Feb 17, 2018

@AriaMinaei: for what it's worth, I'm not aware of any way we can do that so far. I think it comes down to getting a prototype, but as far as I know the type-level operators we have don't do much with JS prototypes.
Out of curiosity, what's your use-case?

@AriaMinaei
Copy link

@tycho01 Thanks, now I know I shouldn't try more :)

Here is my use-case though:

export type Atomify<V> = {
  '1': 
  V extends Array<infer T> ? ArrayAtom<Atomify<T>> :
  V extends AbstractAtom<$IntentionalAny> ? V :
  // following is commented out as I don't know how to detect plain objects in TS
  // V extends {constructor: Function} ? BoxAtom<V> :
  V extends object ? DictAtom<{[K in keyof V]: Atomify<V[K]>}> :
  BoxAtom<V>
}[V extends number ? '1' : '1']

const atomifyDeep = <V extends {}>(jsValue: V): Atomify<V> => {
  if (Array.isArray(jsValue)) {
    return fromJSArray(jsValue)
  } else if (isPlainObject(jsValue)) {
    return fromJSObject(jsValue as $IntentionalAny)
  } else if (jsValue instanceof AbstractAtom) {
    return jsValue as $IntentionalAny
  } else {
    return fromJSPrimitive(jsValue)
  }
}

const fromJSArray = (jsArray: $IntentionalAny): $IntentionalAny => {
  return new ArrayAtom(jsArray.map(atomifyDeep))
}

const fromJSObject = (jsObject: {[key: string]: mixed}): $IntentionalAny => {
  return new DictAtom(mapValues(jsObject, atomifyDeep))
}

const fromJSPrimitive = (jsPrimitive: mixed): $IntentionalAny => {
  return new BoxAtom(jsPrimitive)
}

export default atomifyDeep

This atomifyDeep() function takes any js value as input, and returns a "reactive" data structure. Arrays and plain objects are deeply wrapped into these structures which tracks their changes deeply, while JS primitives, functions, and instances of any class are wrapped in a BoxAtom which tracks their changes via reference.

As you see, I used your [V extends number ? '1' : '1'] trick to recursively call AtomifyDeep, and it passes all of my type test cases, except those for non-plain JS objects. It detects them as plain JS objects and wraps them in a DictAtom.

@phiresky
Copy link

Why are the "other" cases for ReturnType and InstanceType any and not never?

When would you want ReturnType<"notafunction"> to be any? This seems really bad since then you would just propagate any everywhere if you make a mistake while using ReturnType somwhere.

@JasonKleban
Copy link

JasonKleban commented Mar 6, 2018

Awesome!!

I get a few errors attempting to combine the use of type mapping with ReturnType. Is there a way to assert that T[M] will be compatible as the generic argument to ReturnType?

interface X {
    a : (,) => Promise<string>
    b : (,,,) => Promise<number>
    c : () => Promise<UserData>
}

type Cached<T> = { [M in keyof T] : ReturnType<T[M]> } // ❌ Type 'T[M]' does not satisfy the constraint '(...args: any[]) => any'.
const ex : Cached<X> = <any>null; 

type Cached2<T extends { [m : string] : (...args: any[]) => any }> = { [M in keyof T] : ReturnType<T[M]> }
const ex2 : Cached2<X> = <any>null; // ❌ Index signature is missing in type 'X'.

@JasonKleban
Copy link

Nevermind. WOW!! This works:

type Cached<T> = { [M in keyof T] : ReturnType<T[M] extends (...args: any[]) => any ? T[M] : any> }
const ex : Cached<X> = <any>null;

And so does this, to extract the return type out of the promise!! Amazing

type Cached2<T> = { [M in keyof T] : ReturnType<T[M] extends (...args: any[]) => any ? T[M] : any> extends Promise<infer Y> ? Y : ReturnType<T[M] extends (...args: any[]) => any ? T[M] : any> }
const ex2 : Cached2<X> = <any>null;

@michaeljota
Copy link

michaeljota commented Mar 19, 2018

I am playing around with this, and I would like to know a type mapper that resembles Flow $Diff, meaning, one that returns properties that are in both, T and U, but, without expressing T nor U as union types, but intersecting their own properties.

Edit: I mean to say, that $Diff returns a property exclusion between the properties of A, and the properties of B. As in the example below, where I also use the work 'intersection' although is quite the opposite as the real thing I wanted to say. Sorry.

@KiaraGrouwstra
Copy link
Contributor

@michaeljota kinda like Pick<A, Extract<keyof A, keyof B>>?

@michaeljota
Copy link

michaeljota commented Mar 19, 2018

@tycho01 Almost! Thanks you so much! I test it, and is just

class A {
  a: string;
  b: string;
}

class B {
  a: string;
  c: string;
}

type Diff<T, U> = Pick<T, Exclude<keyof T, keyof U>>;

type C = Diff<A, B>;
// C = { b: string; }

Maybe I did not explain myself, but $Diff in Flow is a intersection exclusion of A and B properties.

@michaeljota
Copy link

Any chance for Typescript to have a type mapper with this? Although the naming will be harder than usual.

@jods4
Copy link

jods4 commented May 8, 2018

ReturnType design doesn't support generic functions, right?

Is there a syntax that would work for this:

// Doesn't work in 2.8
function f<T>(x: T): T { return x }

type R = ReturnType<typeof f<string>>; // = string

@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2018

ReturnType design doesn't support generic functions, right?

it is not ReturnType, values can not have generic type arguments. type arguments are only available in certain syntactic locations, like call/new expressions, extends clauses and type references.

@jods4
Copy link

jods4 commented May 9, 2018

@mhegazy That's what I figured, thanks.

Should I open a suggestion for allowing a type argument when a generic function is used in typeof?
ReturnType is a good use case for that.

I know there is an open suggestion for allowing arbitrary expression in typeof anyway, would it be enough?

@mhegazy
Copy link
Contributor

mhegazy commented May 9, 2018

typeof is not the issue either. typeof allows a limited subset of expression syntax. I think #15877 is what you are looking for.

@michaeljota
Copy link

@mhegazy about the PR, do you plan to include Omit type helper. I know how to implemented, but still I think Omit is commonly used and will be worth to include it.

@mhegazy
Copy link
Contributor

mhegazy commented May 9, 2018

do you plan to include Omit type helper.

No. we have avoided to use the names Omit and Diff to avoid breaking users who already defined these types. Omit is trivially implemented as Pick<T, Exclude<keyof T, K>>.

@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

Successfully merging this pull request may close these issues.

8 participants