Conversation

ahejlsberg

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.

@ahejlsbergahejlsberg merged commit f1d7afe into master Feb 9, 2018
@ahejlsbergahejlsberg deleted the predefinedConditionalTypes branch February 9, 2018 23:40
@mhegazymhegazy mentioned this pull request Feb 10, 2018
@KiaraGrouwstra

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

@tycho01 Right you are! Fixed.

@AriaMinaei

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

@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 , but as far as I know the type-level operators we have don't do much with JS s.
Out of curiosity, what's your use-case?

@AriaMinaei

@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

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

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

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

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

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

@michaeljota

@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

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

@jods4

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

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

@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

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

@michaeljota

@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

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>>.

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

Successfully merging this pull request may close these issues.