Skip to content

Improved excess property checks in union types / new paradigm: use literal type infered from literal initializer #42997

Closed
@craigphicks

Description

@craigphicks

Suggestion

🔍 Search Terms

  • strict excess property checks in union types.
  • union type calculation
  • literal type inference from literal initializer

List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Consider the statement:

type T1 = {<some props>}
type T2 = {<some props>}
type T3 = {<some props>}
type TU=T1|T2|T3
SomeTypeDef<T> = ...
const t:SomeTypeDef<TU> = {a:1,b:2}

The last line is an assignment statement. The processing taking place in the assignment has two distinct and separate parts:

  • The left hand side in isolation, which is the type function SomeTypeDef with the single input variable TU.
  • Determining the validity of the assignment of the r.h.s. literal initializer {<some props>} to the l.h.s type. That computation takes place using Typescript's fixed assignment rules that cannot be changed.

Now suppose we define an additional type

type I = {a:1,b:2}

which you will notice is the type of the literal initializer on the r.h.s. of the assignment. Now suppose we add that type as an additional variable to a type function on the l.h.s.:

const t:SomeTypeDefPlus<TU,I> = {a:1,b:2}

Now the l.h.s type function has additional information to work with . Therefore whatever SomeTypeDef<TU> can express, SomeTypeDefPlus<TU,I> can also express in the same length code. However SomeTypeDefPlus<TU,I> may express more things than SomeTypeDef<TU>, and/or may be able to express the same things in shorter code. In psuedo-psuedo code:

Expressability(SomeTypeDefPlus<TU,I>) >= Expressability(SomeTypeDef<TU>)

Unfortunately

  • writing the type type I = {<some props>}, AND
  • and writing the r.h.s literal initializer .... = {<some props>}

is twice as much writing - a code length penalty.

The proposal is to enable the type of the r.h.s. literal initializer to be inferred, e.g.,

const t:SomeTypeDefPlus<TU,I> = {a:1,b:2} as infer literal I

or any other syntax that would automatically set the value of I on the l.h.s. to be type I = {a:1,b:2}

Is there really any need for a SomeTypeDefPlus<TU,I> functionality? Yes. motivating function below explains that
it makes it easier to write a generalized function for strict excess property checking in type unions .

📃 Motivating Example

Typescript Playground

// c.f. https://github.com/microsoft/TypeScript/issues/42997
// craigphicks Feb 2021
//-----------------------
// TYPES
type T1 = {a:number,b:number}
type T2 = {a:number,c:number}
type T3 = {a:string,c?:number}
type T4 = {a:bigint, [key:string]:bigint}
type T5 = {a:string, d:T1|T2|T3|T4}
type T12 = T1|T2|T3|T4|T5
//-----------------------
// TYPES INFERRED FROM THE INITIALIZER 
type I0 = {}
type I1 = {a:1,b:1}
type I2 = {a:1,c:1}
type I3 = {a:1,b:1,c:1}
type I4 = {a:1}
type I5 = {a:'2',c:1}
type I6 = {a:'2'}
type I7 = {a:1n, 42:1n}
type I8 = {a:'1', d:{a:1n, 42:1n}}
type I9 = {a:'1', d:{}}
//-----------------------
// THE CODE 
type Select<T,I>= {[P in keyof I]: P extends keyof T ?
  (T[P] extends object ? ExclusifyUnionPlus<T[P],I[P]> : T[P]) : never} 
type ExclusifyUnionPlus<T,I>= T extends any ? (I extends Select<T,I> ? T : never):never
//-----------------------
// case specific type aliases
type DI<I>=ExclusifyUnionPlus<T12,I>
// special types for se question https://stackoverflow.com/q/46370222/4376643
type sth = { value: number, data: string } | { value: number, note: string };
type DIsth<I>=ExclusifyUnionPlus<sth,I>
//-----------------------
// THE TESTS - ref=refuse, acc=accept
const sth0:DIsth<{ value: 7, data: 'test' }>={ value: 7, data: 'test' }; // should acc
const sth1:DIsth<{ value: 7, note: 'test' }>={ value: 7, note: 'test' }; // should acc
const sth2:DIsth<{ value: 7, data:'test', note: 'hello' }>={ value:7, data:'test',note:'hello' }; // should ref
type DI0=DI<I0> ; const d0:DI0={} // should ref
type DI1=DI<I1> ; const d1:DI1={a:1,b:1} // T1, should acc
type DI2=DI<I2> ; const d2:DI2={a:1,c:1} // T2, should acc
type DI3=DI<I3> ; const d3:DI3={a:1,b:1,c:1} // should ref
type DI4=DI<I4> ; const d4:DI4={a:1} // should ref
type DI5=DI<I5> ; const d5:DI5={a:'2',c:1}  // T3, should acc
type DI6=DI<I6> ; const d6:DI6={a:'2'}  // T3, should acc
type DI7=DI<I7> ; const d7:DI7={a:1n,42:1n}  // T4, should acc
type DI8=DI<I8> ; const d8:DI8={a:'1',d:{a:1n,42:1n}}  // T5, should acc
type DI9=DI<I9> ; const d9:DI9={a:'1',d:{}}  // should ref
//-------------------
// Comparison with type function NOT using type of intializer
// Code from SE  https://stackoverflow.com/a/46370791/4376643
type AllKeys<T> = T extends unknown ? keyof T : never;
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type _ExclusifyUnion<T, K extends PropertyKey> =
    T extends unknown ? Id<T & Partial<Record<Exclude<K, keyof T>, never>>> : never;
type ExclusifyUnion<T> = _ExclusifyUnion<T, AllKeys<T>>;
//-------------------
// case specific alias
type SU=ExclusifyUnion<T12>
// tests
const sd0:SU={} // should ref
const sd1:SU={a:1,b:1} // should acc
const sd2:SU={a:1,c:1} // should acc
const sd3:SU={a:1,b:1,c:1} // should ref
const sd4:SU={a:1} // should ref
const sd5:SU={a:'2',c:1}  // should acc
const sd6:SU={a:'2'}  // should acc
const sd7:SU={a:1n,42:1n}  // should acc
const sd8:SU={a:'1',d:{a:1n,42:1n}}  // should acc
const sd9:SU={a:'1',d:{}}  // should ref
// Apparently ExclusifyUnion doesn't handle addtional property speficier in T4
// Also does it handle deep objects?  Have posted message to ExclusifyUnion author, awaiting reply.

Two criteria by which to compare ExclusifyUnion and ExclusifyUnionPlus -

  • Ease and clarity:
  • Total range of expression:

As for 'ease and clarity' , ExclusifyUnionPlus does seems easier to code and comprehend.
As for 'total range of expression', I am not sure. It would require a proof.

💻 Use Cases

The proposed syntax

const t:SomeTypeDefPlus<TU,I> = {a:1,b:2} as infer literal I

or something like it would simplify the leveraging existing functionality for the purposes of strictly detecting excess properties in literal assignments to union of Record types. That includes deep Record types with deep member which are themselves unions of Record types.

It could also have other uses besides detecting excess properties.

Reference

This suggestion is based on an answer I posted on SE to a question about excess property checks.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions