Search code examples
typescripttypescript-genericstypescript3.0

In TypeScript, is there a way to restrict extra/excess properties for a Partial<T> type when the type is a parameter to a function?


Is there a standard way to get Scenario 1 to have a compile error for not specifying known properties, just like in Scenario 2? Or is there some workaround?

class Class2 {
  g: number;
}

class Testing {
  static testIt3<T>(val: Partial<T>): void {
  }
}

const test = {
  g: 6,
  a: '6',
};

// Scenario 1
Testing.testIt3<Class2>(test);
// TS does not show any errors for this scenario

// Scenario 2
Testing.testIt3<Class2>({
  g: 6,
  a: '6',
});
// but it does for this scenario:
// Object literal may only specify known properties...

Live code


Solution

  • The type system isn't geared toward such restrictions on extra object keys. Types in TypeScript are not exact as requested in microsoft/TypeScript#12936: if an object is of type A, and you add more properties to it that are not mentioned in the definition of A, the object is still of type A. This is essentially required to support class inheritance where subclasses can add properties to superclasses.

    The only time the compiler treats types as exact is when you are using a "fresh" object literal (that is, one that hasn't been assigned to anything yet) and passing it to something that expects an object type. This is called excess property checking and it is sort of a workaround for the lack of exact types in the language. You want excess property checking to occur with "non-fresh" objects like test, but that won't happen.

    TypeScript has no concrete representation for exact types; you can't take a type T and produce Exact<T> from it. But you can use a generic constraint to get this effect. Given a type T, and an object of type U that you want to conform to the unrepresentable Exact<T> type, you can make this:

    type Exactly<T, U extends T> = {[K in keyof U]: K extends keyof T ? T[K] : never};
    type IsExactly<T, U extends T> = U extends Exactly<T, U> ? true : false;
    
    const testGood = {
        g: 1
    }
    type TestGood = IsExactly<Class2, typeof testGood>; // true
    
    const testBad = {
        g: 6,
        a: '6',
    };
    type TestBad = IsExactly<Class2, typeof testBad>; // false
    

    So the compiler is able to tell that typeof testGood is "Exactly<Class2, typeof testGood>" while typeof testBad is not Exactly<Class2, typeof testBad>. We can use this to build a generic function to do what you want. (In your case you want something like ExactlyPartial<T, U> instead of Exactly<T, U>, but it's very similar... just don't constrain U to extend T).


    Unfortunately, your function is already generic in T, the type to make exact. And you are manually specifying T, the generic function needs to infer the type of U. TypeScript doesn't allow partial type argument inference as requested in microsoft/TypeScript#26242. You have to either manually specify all type parameters in a function, or you have to let the compiler infer all type parameters in a function. So there are workarounds:

    One is to split your function in to a curried function in which the first generic function lets you specify T and the returned generic function infers U. It looks like this:

        class Testing {
            static testIt<T>(): <U extends { [K in keyof U]:
                K extends keyof T ? T[K] : never
            }> (val: U) => void {
                return () => { }
            }
        }
    
        Testing.testIt<Class2>()(testBad); // error, prop "a" incompatible
        Testing.testIt<Class2>()(testGood); // okay
    

    This works as you expect, but has runtime impact in that you have to call a curried function for no reason at runtime.

    Another workaround is to pass a value from which T can be inferred to the function. Since you don't need such a value, this is essentially a dummy parameter. Again, it has a runtime impact in that you have to pass in a value that is not used. (You mentioned that you might actually be using such a value at runtime, in which case, this is no longer a workaround but the suggested solution, since you need to pass in something anyway, and the manual specification of T in your code example was a red herring.) It looks like this:

        class Testing {
            static testIt<T, U extends { [K in keyof U]:
                K extends keyof T ? T[K] : never
            }>(ctor: new (...args: any) => T, val: U) {
                // not using ctor in here, so this is a dummy value                        
            }
        }
    
        Testing.testIt(Class2, testBad); // error, prop "a" incompatible
        Testing.testIt(Class2, testGood); // okay    
    

    The third workaround I can think of is to just use the type system to represent the result of the curried function return without actually calling it. It has no runtime impact at all, which makes it more amenable to giving types to existing JS code, but it's a bit clunky to use since you have to assert that Testing.testIt acts the right way. It looks like this:

        interface TestIt<T> {
            <U extends { [K in keyof U]: K extends keyof T ? T[K] : never }>(val: U): void;
        }
    
        class Testing {
            static testIt(val: object) {
                // not using ctor in here, so this is a dummy value                        
            }
        }
            
        (Testing.testIt as TestIt<Class2>)(testBad); // error, prop "a" incompatible
        (Testing.testIt as TestIt<Class2>)(testGood); // okay
    

    Okay, hope one of those works for you.

    Link to code